@lzehrung/codegraph 2.3.6 → 2.3.7

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.
package/README.md CHANGED
@@ -18,8 +18,6 @@ codegraph is a local CLI **and TypeScript library** that turns a source tree int
18
18
 
19
19
  Without structural context, an agent burns early turns listing directories, guessing search terms, opening candidate files, and reconstructing relationships. codegraph does that discovery once so the context window can stay focused on the problem.
20
20
 
21
- Warm-cache timings for `codegraph orient --root . --budget small --json` on this repository have ranged from about **0.6s** (Windows 11, Node 24) to about **2.5s** (a 4-core Linux container, Node 22), so treat them as machine-dependent rather than a headline figure; the matching MCP `orient` call returned in about **100ms** on the faster of the two. [The performance program index](docs/plans/2026-07-25-performance-program-index.md) records the current measurements and the conditions behind them.
22
-
23
21
  With Node.js 22.16 or newer, install from npm:
24
22
 
25
23
  ```bash
@@ -132,13 +130,16 @@ Do not begin by generating every possible report. Start with the question you ac
132
130
  ### Understand an unfamiliar repo
133
131
 
134
132
  ```bash
135
- # Ask one concrete architecture question
136
- codegraph explore "how does the CLI reach review analysis?" --root .
137
-
138
- # If you do not know the question yet, get a bounded map
133
+ # Map the repository before selecting a target
139
134
  codegraph orient --root . --budget small
140
135
 
141
- # Follow an anchor returned by either command
136
+ # Search a concrete term
137
+ codegraph search "review analysis CLI" --json
138
+
139
+ # Use explore only when those results need packets and dependency paths
140
+ codegraph explore "review analysis CLI" --root .
141
+
142
+ # Follow a returned target
142
143
  codegraph explain src/review.ts
143
144
  codegraph deps src/review.ts --json
144
145
  codegraph refs src/review.ts:215:23
@@ -131,6 +131,7 @@ Current-state commands validate the on-disk index automatically and default to t
131
131
  - Repeated agent queries over one repo snapshot: prefer MCP so the index stays warm. MCP inputs are flat JSON objects and the server root is fixed at startup; send only fields in the mounted tool schema, never CLI flags such as `--root` or `--json`. Unknown tool fields are rejected as invalid parameters. If the first MCP call fails at startup or loses its transport, do not retry the same server; run `codegraph doctor`, use the equivalent CLI command for this session, and restart the agent client after package upgrades.
132
132
  - Durable graph handoff: use `codegraph graph --root . ./src --json --output codegraph.json` rather than parsing display text. `graph --json` and `index --json` payloads include an `analysis` object (`mode`, `backend`, `label`, fallback counts); when `mode` is `mixed` or `reduced` the run used regex/graph-only extraction for some or all files, so treat symbol accuracy accordingly. Degraded runs also print a `Backend:` warning on stderr even without `--progress`.
133
133
  - Search and inspect performance diagnosis: add `--report` for JSON on stderr or `--report-file <path>` for a file while keeping normal command output unchanged.
134
+ - Index discovery diagnosis: `--report` timing JSON can include `sourceDiscoveryMs` and `metadataDiscoveryMs` when those discovery steps run.
134
135
 
135
136
  ### Human Graph Viewer
136
137
 
package/dist/bin/cli.js CHANGED
@@ -13534,6 +13534,7 @@ __export(git_exports, {
13534
13534
  isGitWorktreeSentinel: () => isGitWorktreeSentinel,
13535
13535
  listChangedFiles: () => listChangedFiles,
13536
13536
  listGitExcludeFiles: () => listGitExcludeFiles,
13537
+ listGitIgnoreFiles: () => listGitIgnoreFiles,
13537
13538
  listGitSubmoduleDirectories: () => listGitSubmoduleDirectories,
13538
13539
  listTrackedFiles: () => listTrackedFiles,
13539
13540
  listUntrackedFiles: () => listUntrackedFiles,
@@ -13969,6 +13970,32 @@ async function listTrackedFiles(projectRoot, opts) {
13969
13970
  throw createGitError(projectRoot, args, error51);
13970
13971
  }
13971
13972
  }
13973
+ async function listGitIgnoreFiles(projectRoot, opts) {
13974
+ if (!(opts?.gitAvailable ?? true))
13975
+ return [];
13976
+ const pathspec = [".gitignore", ":(glob)**/.gitignore"];
13977
+ const commands = [
13978
+ ["ls-files", "--cached", "-z", "--", ...pathspec],
13979
+ ["ls-files", "--others", "--exclude-standard", "-z", "--", ...pathspec],
13980
+ ["ls-files", "--others", "--ignored", "--exclude-standard", "-z", "--", ...pathspec]
13981
+ ];
13982
+ const results = await Promise.all(commands.map(async (args) => {
13983
+ try {
13984
+ return await runGit(projectRoot, args, { maxBuffer: 64 * 1024 * 1024 });
13985
+ } catch (error51) {
13986
+ throw createGitError(projectRoot, args, error51);
13987
+ }
13988
+ }));
13989
+ const out = [];
13990
+ for (const { stdout } of results) {
13991
+ for (const rel of stdout.split("\0").filter(Boolean)) {
13992
+ const abs = resolveGitListedPath(projectRoot, rel);
13993
+ if (abs)
13994
+ out.push(abs);
13995
+ }
13996
+ }
13997
+ return Array.from(new Set(out));
13998
+ }
13972
13999
  async function listGitSubmoduleDirectories(projectRoot, opts) {
13973
14000
  if (!(opts?.gitAvailable ?? true))
13974
14001
  return [];
@@ -14380,24 +14407,24 @@ function collectCandidateAncestorDirectories(root, files) {
14380
14407
  }
14381
14408
  return directories;
14382
14409
  }
14383
- async function findGitIgnoreSources(sourceRoots, files) {
14410
+ async function findGitIgnoreSources(sourceRoots) {
14384
14411
  const roots = Array.from(new Map(sourceRoots.map(({ path: sourceRoot, repositoryRoot }) => [
14385
14412
  fileIdentityKey(sourceRoot),
14386
14413
  { path: normalizePath(sourceRoot), repositoryRoot: normalizePath(repositoryRoot) }
14387
14414
  ])).values());
14388
14415
  const sources = [];
14389
14416
  for (const { path: sourceRoot, repositoryRoot } of roots) {
14390
- const directories = collectCandidateAncestorDirectories(sourceRoot, files.filter((file2) => isFilePathWithinRoot(sourceRoot, file2)));
14391
- const present = await mapLimitSemaphore(directories, REALPATH_FILTER_CONCURRENCY, async (directory) => {
14392
- const candidate = normalizePath(path9.join(directory, ".gitignore"));
14393
- try {
14394
- return (await fsp5.stat(candidate)).isFile() ? { file: candidate, baseDir: directory, repositoryRoot, priority: 2 } : null;
14395
- } catch {
14396
- return null;
14397
- }
14398
- });
14399
- sources.push(...present.filter((entry) => entry !== null));
14400
- sources.push(...(await listGitExcludeFiles(sourceRoot)).map((source) => ({
14417
+ const [gitignoreFiles, excludeFiles] = await Promise.all([
14418
+ listGitIgnoreFiles(sourceRoot),
14419
+ listGitExcludeFiles(sourceRoot)
14420
+ ]);
14421
+ sources.push(...gitignoreFiles.map((file2) => ({
14422
+ file: file2,
14423
+ baseDir: path9.dirname(file2),
14424
+ repositoryRoot,
14425
+ priority: 2
14426
+ })));
14427
+ sources.push(...excludeFiles.map((source) => ({
14401
14428
  ...source,
14402
14429
  repositoryRoot: source.baseDir
14403
14430
  })));
@@ -14428,13 +14455,6 @@ async function listGitCandidateFiles(root, logLevel) {
14428
14455
  const rawFiles = Array.from(/* @__PURE__ */ new Set([...tracked, ...untracked, ...submoduleUntracked.flat()])).sort();
14429
14456
  const needsLogicalRemap = normalizePath(gitRoot) !== normalizePath(root);
14430
14457
  const files = Array.from(new Set(rawFiles.map((file2) => needsLogicalRemap && isFilePathWithinRoot(realRoot, file2) ? normalizePath(path9.resolve(root, path9.relative(realRoot, file2))) : normalizePath(file2)))).sort();
14431
- const physicalFiles = normalizePath(root) === realRoot ? [] : (await mapLimitSemaphore(files, REALPATH_FILTER_CONCURRENCY, async (file2) => {
14432
- try {
14433
- return normalizePath(await fsp5.realpath(file2));
14434
- } catch {
14435
- return null;
14436
- }
14437
- })).filter((file2) => file2 !== null);
14438
14458
  const gitignoreRoots = [gitRepositoryRoot, ...submoduleRoots.map(({ physicalPath }) => physicalPath)].map(normalizePath);
14439
14459
  const sourceRoots = [
14440
14460
  { path: root, repositoryRoot: gitRepositoryRoot },
@@ -14447,7 +14467,7 @@ async function listGitCandidateFiles(root, logLevel) {
14447
14467
  ];
14448
14468
  return {
14449
14469
  files,
14450
- gitignoreFiles: await findGitIgnoreSources(sourceRoots, [...rawFiles, ...files, ...physicalFiles]),
14470
+ gitignoreFiles: await findGitIgnoreSources(sourceRoots),
14451
14471
  gitignoreRoots,
14452
14472
  gitignoreAliases: sourceRoots
14453
14473
  };
@@ -53074,12 +53094,17 @@ var init_parsed_cache = __esm({
53074
53094
  // dist/indexer/finalize.js
53075
53095
  import { performance as performance2 } from "node:perf_hooks";
53076
53096
  async function finalizeProjectIndex(args) {
53077
- if (args.timings)
53078
- args.timings.totalMs = Math.round(performance2.now() - args.totalStart);
53097
+ const discoveryTimings = args.opts?.report ? args.opts.report.timings ??= {} : void 0;
53098
+ const metadataDiscoveryStart = args.projectFiles === void 0 ? performance2.now() : void 0;
53079
53099
  const projectFiles = await (args.projectFiles ?? discoverProjectFilesWithGitCandidates(args.projectRoot, {
53080
53100
  ...args.opts?.logLevel ? { logLevel: args.opts.logLevel } : {},
53081
53101
  ...args.knownGitCandidates !== void 0 ? { knownGitCandidates: args.knownGitCandidates } : {}
53082
53102
  }));
53103
+ if (metadataDiscoveryStart !== void 0 && discoveryTimings) {
53104
+ discoveryTimings.metadataDiscoveryMs = Math.round(performance2.now() - metadataDiscoveryStart);
53105
+ }
53106
+ if (args.timings)
53107
+ args.timings.totalMs = Math.round(performance2.now() - args.totalStart);
53083
53108
  const languageExtensions = normalizeLanguageExtensions(args.opts?.languageExtensions);
53084
53109
  const parsed = retainedParsedCache(args.parsedMap, args.opts);
53085
53110
  return {
@@ -55698,6 +55723,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
55698
55723
  }
55699
55724
  }
55700
55725
  async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpts) {
55726
+ const timings = opts?.report ? opts.report.timings ??= {} : void 0;
55701
55727
  await initializeFileIdentityCaseSensitivity(projectRoot);
55702
55728
  try {
55703
55729
  const useDiskCache = (opts?.cache ?? "off") === "disk";
@@ -55717,6 +55743,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
55717
55743
  const onDiscoveryProgress = helperOpts?.reportDiscoveryProgress ? (progress) => emitIndexCheckActivity(opts, progress.activity, progress.current, progress.total) : void 0;
55718
55744
  if (helperOpts?.reportDiscoveryProgress)
55719
55745
  emitIndexCheckActivity(opts, "Discovering source files");
55746
+ const sourceDiscoveryStart = performance5.now();
55720
55747
  const discoveredFiles = await listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
55721
55748
  ...opts?.discovery,
55722
55749
  ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
@@ -55725,6 +55752,8 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
55725
55752
  onGitCandidatesDiscovered,
55726
55753
  ...onDiscoveryProgress ? { onDiscoveryProgress } : {}
55727
55754
  });
55755
+ if (timings)
55756
+ timings.sourceDiscoveryMs = Math.round(performance5.now() - sourceDiscoveryStart);
55728
55757
  const additionalFileCandidates = await normalizeIndexedFileInputsWithinRoot(projectRoot, opts?.additionalFiles ?? [], "Additional index file");
55729
55758
  const additionalFileExistence = await probePathExistence(additionalFileCandidates, buildConcurrency(opts));
55730
55759
  const additionalFiles = additionalFileCandidates.filter((file2) => additionalFileExistence.get(file2));
@@ -55733,12 +55762,15 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
55733
55762
  const transientFiles = additionalFiles.filter((file2) => !discoveredFileSet.has(file2));
55734
55763
  if (helperOpts?.reportDiscoveryProgress)
55735
55764
  emitIndexCheckActivity(opts, "Discovering project metadata");
55765
+ const metadataDiscoveryStart = performance5.now();
55736
55766
  const projectFiles = await discoverProjectFilesWithGitCandidates(projectRoot, {
55737
55767
  ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
55738
55768
  ...discoveredSymlinkDirectories !== void 0 ? { knownSymlinkDirectories: discoveredSymlinkDirectories } : {},
55739
55769
  ...discoveredGitCandidates !== void 0 ? { knownGitCandidates: discoveredGitCandidates } : {},
55740
55770
  ...onDiscoveryProgress ? { onDiscoveryProgress } : {}
55741
55771
  });
55772
+ if (timings)
55773
+ timings.metadataDiscoveryMs = Math.round(performance5.now() - metadataDiscoveryStart);
55742
55774
  return await buildIndexFromFileListShared(projectRoot, files, opts, {
55743
55775
  manifestMode: useDiskCache ? "read-write" : "off",
55744
55776
  warnNoFilesMessage: `Warning: No files found in project root: ${projectRoot}. Check codegraph.config.json globs and CLI --include-glob/--ignore-glob filters. Diagnostic: codegraph doctor`,
@@ -55794,6 +55826,7 @@ async function buildProjectIndexIncremental(projectRoot, opts) {
55794
55826
  if (strictIncremental && graphOptions.fast)
55795
55827
  graphOptions.fast = false;
55796
55828
  const { normalizedProjectRoot, report, timings, totalStart, cacheMode, cacheEnabled, onFallbackImportExtraction } = createIndexBuildRunState(projectRoot, opts, graphOptions);
55829
+ const discoveryTimings = opts?.report ? opts.report.timings ??= {} : void 0;
55797
55830
  let checkProgressActive = false;
55798
55831
  const startCheckProgress = () => {
55799
55832
  if (checkProgressActive)
@@ -55886,13 +55919,31 @@ async function buildProjectIndexIncremental(projectRoot, opts) {
55886
55919
  const hasExplicitGitRange = !!opts?.gitBase || !!opts?.gitHead;
55887
55920
  const shouldDiffAgainstWorkingTree = !hasExplicitGitRange && gitAvailable && !!manifest.lastCommit;
55888
55921
  const canReuseReconciliation = opts?.reconciledManifestUpdatedAt === manifest.updatedAt;
55922
+ let sourceDiscoveryMs = 0;
55923
+ let sourceDiscoveryPerformed = false;
55924
+ const measureSourceDiscovery = async (operation) => {
55925
+ if (!discoveryTimings || opts?.filesAreProjectScope)
55926
+ return await operation();
55927
+ sourceDiscoveryPerformed = true;
55928
+ const start = performance5.now();
55929
+ try {
55930
+ return await operation();
55931
+ } finally {
55932
+ sourceDiscoveryMs += performance5.now() - start;
55933
+ }
55934
+ };
55889
55935
  let manifestDiffFiles = [];
55890
55936
  if (shouldDiffAgainstWorkingTree) {
55891
55937
  try {
55892
- manifestDiffFiles = (canReuseReconciliation ? opts?.reconciledWorkingTreeDiffFiles : void 0) ?? await listChangedFiles(projectRoot, {
55893
- base: manifest.lastCommit,
55894
- head: "WORKTREE"
55895
- });
55938
+ const reconciledWorkingTreeDiffFiles = canReuseReconciliation ? opts?.reconciledWorkingTreeDiffFiles : void 0;
55939
+ if (reconciledWorkingTreeDiffFiles !== void 0) {
55940
+ manifestDiffFiles = reconciledWorkingTreeDiffFiles;
55941
+ } else {
55942
+ manifestDiffFiles = await measureSourceDiscovery(() => listChangedFiles(projectRoot, {
55943
+ base: manifest.lastCommit,
55944
+ head: "WORKTREE"
55945
+ }));
55946
+ }
55896
55947
  } catch (error51) {
55897
55948
  if (!isMissingGitRevisionError(error51))
55898
55949
  throw error51;
@@ -55937,13 +55988,21 @@ async function buildProjectIndexIncremental(projectRoot, opts) {
55937
55988
  const previousTransientFiles = sanitizeManifestTransientFilesForRoot(projectRoot, projectRoot, manifest.transientFiles).filter((file2) => Object.hasOwn(trackedEntries, file2));
55938
55989
  const previousTransientFileSet = new Set(previousTransientFiles);
55939
55990
  const needsGitScan = !!opts?.gitBase || !!opts?.changedSince;
55940
- const gitFiles = needsGitScan ? await listChangedFiles(projectRoot, buildIncrementalGitDiffOptions(opts)) : [];
55991
+ let gitFiles = [];
55992
+ if (needsGitScan) {
55993
+ gitFiles = await measureSourceDiscovery(() => listChangedFiles(projectRoot, buildIncrementalGitDiffOptions(opts)));
55994
+ }
55941
55995
  let untrackedFiles = [];
55942
55996
  let rediscoveredFiles = [];
55943
55997
  let discoveredGitCandidates;
55944
55998
  if (canUseIncrementalDiscoveryFastPath(gitAvailable, opts?.cacheStrict)) {
55945
55999
  try {
55946
- untrackedFiles = (canReuseReconciliation ? opts?.reconciledUntrackedFiles : void 0) ?? await listUntrackedProjectFiles(projectRoot, opts?.discovery, gitAvailable);
56000
+ const reconciledUntrackedFiles = canReuseReconciliation ? opts?.reconciledUntrackedFiles : void 0;
56001
+ if (reconciledUntrackedFiles !== void 0) {
56002
+ untrackedFiles = reconciledUntrackedFiles;
56003
+ } else {
56004
+ untrackedFiles = await measureSourceDiscovery(() => listUntrackedProjectFiles(projectRoot, opts?.discovery, gitAvailable));
56005
+ }
55947
56006
  } catch (error51) {
55948
56007
  if (manifestReport) {
55949
56008
  manifestReport.reason = "gitUntrackedScanFailed";
@@ -55956,14 +56015,17 @@ async function buildProjectIndexIncremental(projectRoot, opts) {
55956
56015
  });
55957
56016
  }
55958
56017
  } else if (!opts?.filesAreProjectScope) {
55959
- rediscoveredFiles = await listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
56018
+ rediscoveredFiles = await measureSourceDiscovery(() => listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
55960
56019
  ...opts?.discovery,
55961
56020
  ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
55962
56021
  ...!opts?.cacheStrict && manifest.symlinkDirectories !== void 0 ? { knownSymlinkDirectories: manifest.symlinkDirectories } : {},
55963
56022
  onGitCandidatesDiscovered: (candidates) => {
55964
56023
  discoveredGitCandidates = candidates;
55965
56024
  }
55966
- });
56025
+ }));
56026
+ }
56027
+ if (discoveryTimings && sourceDiscoveryPerformed) {
56028
+ discoveryTimings.sourceDiscoveryMs = Math.round(sourceDiscoveryMs);
55967
56029
  }
55968
56030
  const candidateFiles = [
55969
56031
  ...explicitFiles,
@@ -56062,10 +56124,16 @@ async function buildProjectIndexIncremental(projectRoot, opts) {
56062
56124
  if (!snapshotLoad)
56063
56125
  return null;
56064
56126
  const snapshot = snapshotLoad.index;
56065
- snapshot.projectFiles ??= await discoverProjectFilesWithGitCandidates(projectRoot, {
56066
- ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
56067
- ...discoveredGitCandidates !== void 0 ? { knownGitCandidates: discoveredGitCandidates } : {}
56068
- });
56127
+ snapshot.manifestSignaturesFresh = false;
56128
+ if (snapshot.projectFiles === void 0) {
56129
+ const metadataDiscoveryStart = performance5.now();
56130
+ snapshot.projectFiles = await discoverProjectFilesWithGitCandidates(projectRoot, {
56131
+ ...opts?.logLevel ? { logLevel: opts.logLevel } : {},
56132
+ ...discoveredGitCandidates !== void 0 ? { knownGitCandidates: discoveredGitCandidates } : {}
56133
+ });
56134
+ if (discoveryTimings)
56135
+ discoveryTimings.metadataDiscoveryMs = Math.round(performance5.now() - metadataDiscoveryStart);
56136
+ }
56069
56137
  if (opts?.cache) {
56070
56138
  snapshot.cacheMode = opts.cache;
56071
56139
  snapshot.cacheRootDir = cacheRoot(projectRoot, opts);
@@ -56350,6 +56418,17 @@ async function buildProjectIndexIncremental(projectRoot, opts) {
56350
56418
  edges: []
56351
56419
  });
56352
56420
  }
56421
+ for (const [file2, entry] of manifestEntries) {
56422
+ const signature = fileSignatures.get(file2);
56423
+ if (!signature)
56424
+ continue;
56425
+ const { gitSig: _previousGitSig, ...entryWithoutGitSig } = entry;
56426
+ manifestEntries.set(file2, {
56427
+ ...entryWithoutGitSig,
56428
+ sig: signature.sig,
56429
+ ...signature.gitSig ? { gitSig: signature.gitSig } : {}
56430
+ });
56431
+ }
56353
56432
  await writeIndexManifestSnapshot({
56354
56433
  projectRoot,
56355
56434
  opts,
@@ -58577,7 +58656,7 @@ async function buildLifecycleManifest(root, buildOptions, existing, options = {}
58577
58656
  configHash: await hashConfig(root, buildOptions?.logLevel),
58578
58657
  buildOptionsHash: hashBuildOptions(buildOptions),
58579
58658
  fileCount: snapshot.files.length,
58580
- fileSignatureHash: await hashDiscoveredFiles(snapshot.files, root),
58659
+ fileSignatureHash: hashManifestEntries(snapshot.files, root, snapshot.index.manifestEntries, snapshot.index.manifestSignaturesFresh) ?? await hashDiscoveredFiles(snapshot.files, root),
58581
58660
  files: discoveredFileRelativePaths(snapshot.files, root),
58582
58661
  analysis: snapshot.analysis
58583
58662
  };
@@ -58653,6 +58732,56 @@ function isMissingStatRace2(error51) {
58653
58732
  return false;
58654
58733
  return error51.code === "ENOENT" || error51.code === "ENOTDIR";
58655
58734
  }
58735
+ function hashManifestEntries(files, root, entries, signaturesFresh) {
58736
+ if (!signaturesFresh || !entries || entries.size !== files.length)
58737
+ return void 0;
58738
+ const sorted = [...files].sort((left, right) => left.localeCompare(right));
58739
+ const signatures = /* @__PURE__ */ new Map();
58740
+ for (const file2 of sorted) {
58741
+ const entry = entries.get(file2);
58742
+ const signature = parseLifecycleFileSignature(entry?.sig);
58743
+ if (!signature)
58744
+ return void 0;
58745
+ signatures.set(file2, signature);
58746
+ }
58747
+ const hash2 = createHash4("sha256");
58748
+ for (const file2 of sorted) {
58749
+ const signature = signatures.get(file2);
58750
+ if (!signature)
58751
+ return void 0;
58752
+ const relative = path51.relative(root, file2).replace(/\\/g, "/");
58753
+ hash2.update(relative);
58754
+ hash2.update("\0");
58755
+ hash2.update(signature.size);
58756
+ hash2.update("\0");
58757
+ hash2.update(signature.mtimeMs);
58758
+ hash2.update("\0");
58759
+ }
58760
+ return hash2.digest("hex");
58761
+ }
58762
+ function parseLifecycleFileSignature(value) {
58763
+ if (typeof value !== "string")
58764
+ return void 0;
58765
+ if (value === "0:0")
58766
+ return void 0;
58767
+ const parts = value.split(":");
58768
+ if (parts.length !== 2)
58769
+ return void 0;
58770
+ const [mtimeMs, size] = parts;
58771
+ if (!mtimeMs || !size)
58772
+ return void 0;
58773
+ if (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(mtimeMs))
58774
+ return void 0;
58775
+ if (!/^(?:0|[1-9]\d*)$/.test(size))
58776
+ return void 0;
58777
+ const numericMtimeMs = Number(mtimeMs);
58778
+ const numericSize = Number(size);
58779
+ if (!Number.isFinite(numericMtimeMs) || String(numericMtimeMs) !== mtimeMs)
58780
+ return void 0;
58781
+ if (!Number.isSafeInteger(numericSize) || String(numericSize) !== size)
58782
+ return void 0;
58783
+ return { size, mtimeMs };
58784
+ }
58656
58785
  async function hashDiscoveredFiles(files, root) {
58657
58786
  const sorted = [...files].sort((left, right) => left.localeCompare(right));
58658
58787
  const signatures = /* @__PURE__ */ new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"build-index.d.ts","sourceRoot":"","sources":["../../src/indexer/build-index.ts"],"names":[],"mappings":"AAuFA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAI5B,KAAK,YAAY,EAIlB,MAAM,YAAY,CAAC;AAykCpB;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAEvG;AAED;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC,CAgBvB;AA4BD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,uBAAuB,GAC7B,OAAO,CAAC,YAAY,CAAC,CA8sBvB;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8IpH"}
1
+ {"version":3,"file":"build-index.d.ts","sourceRoot":"","sources":["../../src/indexer/build-index.ts"],"names":[],"mappings":"AAuFA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAI5B,KAAK,YAAY,EAIlB,MAAM,YAAY,CAAC;AA8kCpB;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAEvG;AAED;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC,YAAY,CAAC,CAgBvB;AA4BD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,uBAAuB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAmwBvB;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8IpH"}
@@ -853,6 +853,7 @@ async function buildIndexFromFileListShared(projectRoot, rawFiles, opts, helperO
853
853
  }
854
854
  }
855
855
  async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpts) {
856
+ const timings = opts?.report ? (opts.report.timings ??= {}) : undefined;
856
857
  await initializeFileIdentityCaseSensitivity(projectRoot);
857
858
  try {
858
859
  const useDiskCache = (opts?.cache ?? "off") === "disk";
@@ -888,6 +889,7 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
888
889
  // Git a second time.
889
890
  if (helperOpts?.reportDiscoveryProgress)
890
891
  emitIndexCheckActivity(opts, "Discovering source files");
892
+ const sourceDiscoveryStart = performance.now();
891
893
  const discoveredFiles = await listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
892
894
  ...opts?.discovery,
893
895
  ...(opts?.logLevel ? { logLevel: opts.logLevel } : {}),
@@ -896,6 +898,8 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
896
898
  onGitCandidatesDiscovered,
897
899
  ...(onDiscoveryProgress ? { onDiscoveryProgress } : {}),
898
900
  });
901
+ if (timings)
902
+ timings.sourceDiscoveryMs = Math.round(performance.now() - sourceDiscoveryStart);
899
903
  const additionalFileCandidates = await normalizeIndexedFileInputsWithinRoot(projectRoot, opts?.additionalFiles ?? [], "Additional index file");
900
904
  const additionalFileExistence = await probePathExistence(additionalFileCandidates, buildConcurrency(opts));
901
905
  const additionalFiles = additionalFileCandidates.filter((file) => additionalFileExistence.get(file));
@@ -904,12 +908,15 @@ async function buildProjectIndexWithManifestOptions(projectRoot, opts, helperOpt
904
908
  const transientFiles = additionalFiles.filter((file) => !discoveredFileSet.has(file));
905
909
  if (helperOpts?.reportDiscoveryProgress)
906
910
  emitIndexCheckActivity(opts, "Discovering project metadata");
911
+ const metadataDiscoveryStart = performance.now();
907
912
  const projectFiles = await discoverProjectFilesWithGitCandidates(projectRoot, {
908
913
  ...(opts?.logLevel ? { logLevel: opts.logLevel } : {}),
909
914
  ...(discoveredSymlinkDirectories !== undefined ? { knownSymlinkDirectories: discoveredSymlinkDirectories } : {}),
910
915
  ...(discoveredGitCandidates !== undefined ? { knownGitCandidates: discoveredGitCandidates } : {}),
911
916
  ...(onDiscoveryProgress ? { onDiscoveryProgress } : {}),
912
917
  });
918
+ if (timings)
919
+ timings.metadataDiscoveryMs = Math.round(performance.now() - metadataDiscoveryStart);
913
920
  return await buildIndexFromFileListShared(projectRoot, files, opts, {
914
921
  manifestMode: useDiskCache ? "read-write" : "off",
915
922
  warnNoFilesMessage: `Warning: No files found in project root: ${projectRoot}. Check codegraph.config.json globs and CLI --include-glob/--ignore-glob filters. Diagnostic: codegraph doctor`,
@@ -998,6 +1005,7 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
998
1005
  if (strictIncremental && graphOptions.fast)
999
1006
  graphOptions.fast = false;
1000
1007
  const { normalizedProjectRoot, report, timings, totalStart, cacheMode, cacheEnabled, onFallbackImportExtraction } = createIndexBuildRunState(projectRoot, opts, graphOptions);
1008
+ const discoveryTimings = opts?.report ? (opts.report.timings ??= {}) : undefined;
1001
1009
  let checkProgressActive = false;
1002
1010
  const startCheckProgress = () => {
1003
1011
  if (checkProgressActive)
@@ -1108,15 +1116,35 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1108
1116
  // covers ordinary new-commit history when the working tree is clean at the new HEAD.
1109
1117
  const shouldDiffAgainstWorkingTree = !hasExplicitGitRange && gitAvailable && !!manifest.lastCommit;
1110
1118
  const canReuseReconciliation = opts?.reconciledManifestUpdatedAt === manifest.updatedAt;
1119
+ let sourceDiscoveryMs = 0;
1120
+ let sourceDiscoveryPerformed = false;
1121
+ const measureSourceDiscovery = async (operation) => {
1122
+ if (!discoveryTimings || opts?.filesAreProjectScope)
1123
+ return await operation();
1124
+ sourceDiscoveryPerformed = true;
1125
+ const start = performance.now();
1126
+ try {
1127
+ return await operation();
1128
+ }
1129
+ finally {
1130
+ sourceDiscoveryMs += performance.now() - start;
1131
+ }
1132
+ };
1111
1133
  let manifestDiffFiles = [];
1112
1134
  if (shouldDiffAgainstWorkingTree) {
1113
1135
  try {
1114
- manifestDiffFiles =
1115
- (canReuseReconciliation ? opts?.reconciledWorkingTreeDiffFiles : undefined) ??
1116
- (await listChangedFiles(projectRoot, {
1117
- base: manifest.lastCommit,
1118
- head: "WORKTREE",
1119
- }));
1136
+ const reconciledWorkingTreeDiffFiles = canReuseReconciliation
1137
+ ? opts?.reconciledWorkingTreeDiffFiles
1138
+ : undefined;
1139
+ if (reconciledWorkingTreeDiffFiles !== undefined) {
1140
+ manifestDiffFiles = reconciledWorkingTreeDiffFiles;
1141
+ }
1142
+ else {
1143
+ manifestDiffFiles = await measureSourceDiscovery(() => listChangedFiles(projectRoot, {
1144
+ base: manifest.lastCommit,
1145
+ head: "WORKTREE",
1146
+ }));
1147
+ }
1120
1148
  }
1121
1149
  catch (error) {
1122
1150
  if (!isMissingGitRevisionError(error))
@@ -1163,7 +1191,10 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1163
1191
  const previousTransientFiles = sanitizeManifestTransientFilesForRoot(projectRoot, projectRoot, manifest.transientFiles).filter((file) => Object.hasOwn(trackedEntries, file));
1164
1192
  const previousTransientFileSet = new Set(previousTransientFiles);
1165
1193
  const needsGitScan = !!opts?.gitBase || !!opts?.changedSince;
1166
- const gitFiles = needsGitScan ? await listChangedFiles(projectRoot, buildIncrementalGitDiffOptions(opts)) : [];
1194
+ let gitFiles = [];
1195
+ if (needsGitScan) {
1196
+ gitFiles = await measureSourceDiscovery(() => listChangedFiles(projectRoot, buildIncrementalGitDiffOptions(opts)));
1197
+ }
1167
1198
  // New files that were never committed, staged, or passed explicitly have no tracked
1168
1199
  // manifest entry and no working-tree-diff record, so they would otherwise stay
1169
1200
  // invisible to an incremental build until the next full rebuild. Detecting them via
@@ -1183,9 +1214,13 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1183
1214
  let discoveredGitCandidates;
1184
1215
  if (canUseIncrementalDiscoveryFastPath(gitAvailable, opts?.cacheStrict)) {
1185
1216
  try {
1186
- untrackedFiles =
1187
- (canReuseReconciliation ? opts?.reconciledUntrackedFiles : undefined) ??
1188
- (await listUntrackedProjectFiles(projectRoot, opts?.discovery, gitAvailable));
1217
+ const reconciledUntrackedFiles = canReuseReconciliation ? opts?.reconciledUntrackedFiles : undefined;
1218
+ if (reconciledUntrackedFiles !== undefined) {
1219
+ untrackedFiles = reconciledUntrackedFiles;
1220
+ }
1221
+ else {
1222
+ untrackedFiles = await measureSourceDiscovery(() => listUntrackedProjectFiles(projectRoot, opts?.discovery, gitAvailable));
1223
+ }
1189
1224
  }
1190
1225
  catch (error) {
1191
1226
  if (manifestReport) {
@@ -1200,7 +1235,7 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1200
1235
  }
1201
1236
  }
1202
1237
  else if (!opts?.filesAreProjectScope) {
1203
- rediscoveredFiles = await listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
1238
+ rediscoveredFiles = await measureSourceDiscovery(() => listProjectFilesWithGitCandidates(projectRoot, projectPatternsForLanguageExtensions(opts), {
1204
1239
  ...opts?.discovery,
1205
1240
  ...(opts?.logLevel ? { logLevel: opts.logLevel } : {}),
1206
1241
  ...(!opts?.cacheStrict && manifest.symlinkDirectories !== undefined
@@ -1209,7 +1244,10 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1209
1244
  onGitCandidatesDiscovered: (candidates) => {
1210
1245
  discoveredGitCandidates = candidates;
1211
1246
  },
1212
- });
1247
+ }));
1248
+ }
1249
+ if (discoveryTimings && sourceDiscoveryPerformed) {
1250
+ discoveryTimings.sourceDiscoveryMs = Math.round(sourceDiscoveryMs);
1213
1251
  }
1214
1252
  const candidateFiles = [
1215
1253
  ...explicitFiles,
@@ -1314,10 +1352,19 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1314
1352
  if (!snapshotLoad)
1315
1353
  return null;
1316
1354
  const snapshot = snapshotLoad.index;
1317
- snapshot.projectFiles ??= await discoverProjectFilesWithGitCandidates(projectRoot, {
1318
- ...(opts?.logLevel ? { logLevel: opts.logLevel } : {}),
1319
- ...(discoveredGitCandidates !== undefined ? { knownGitCandidates: discoveredGitCandidates } : {}),
1320
- });
1355
+ // The early Git fast path does not stat tracked files. Its persisted mtime:size
1356
+ // values can therefore be stale after a byte-identical rewrite; lifecycle must
1357
+ // re-stat until the validated path below replaces every signature.
1358
+ snapshot.manifestSignaturesFresh = false;
1359
+ if (snapshot.projectFiles === undefined) {
1360
+ const metadataDiscoveryStart = performance.now();
1361
+ snapshot.projectFiles = await discoverProjectFilesWithGitCandidates(projectRoot, {
1362
+ ...(opts?.logLevel ? { logLevel: opts.logLevel } : {}),
1363
+ ...(discoveredGitCandidates !== undefined ? { knownGitCandidates: discoveredGitCandidates } : {}),
1364
+ });
1365
+ if (discoveryTimings)
1366
+ discoveryTimings.metadataDiscoveryMs = Math.round(performance.now() - metadataDiscoveryStart);
1367
+ }
1321
1368
  if (opts?.cache) {
1322
1369
  snapshot.cacheMode = opts.cache;
1323
1370
  snapshot.cacheRootDir = cacheRoot(projectRoot, opts);
@@ -1646,6 +1693,20 @@ export async function buildProjectIndexIncremental(projectRoot, opts) {
1646
1693
  edges: [],
1647
1694
  });
1648
1695
  }
1696
+ // A Git blob match proves source bytes, not mtime. Refresh every retained
1697
+ // entry from the stat signatures computed above before declaring lifecycle
1698
+ // signatures fresh.
1699
+ for (const [file, entry] of manifestEntries) {
1700
+ const signature = fileSignatures.get(file);
1701
+ if (!signature)
1702
+ continue;
1703
+ const { gitSig: _previousGitSig, ...entryWithoutGitSig } = entry;
1704
+ manifestEntries.set(file, {
1705
+ ...entryWithoutGitSig,
1706
+ sig: signature.sig,
1707
+ ...(signature.gitSig ? { gitSig: signature.gitSig } : {}),
1708
+ });
1709
+ }
1649
1710
  await writeIndexManifestSnapshot({
1650
1711
  projectRoot,
1651
1712
  opts,