diff --git a/codegraph-skill/codegraph/SKILL.md b/codegraph-skill/codegraph/SKILL.md index 4fe21909..72e8cc99 100644 --- a/codegraph-skill/codegraph/SKILL.md +++ b/codegraph-skill/codegraph/SKILL.md @@ -33,7 +33,7 @@ Then choose the smallest useful follow-up: - packet: `codegraph packet get --pretty` - search: `codegraph search "auth user" --json` - explain: `codegraph explain ` -- architecture: `codegraph inspect ./src --limit 20` +- architecture: `codegraph inspect ./src --limit 20` (`--duplicates` adds the slower duplicate summary) - dependencies: `codegraph deps ` or `codegraph rdeps ` - path: `codegraph path ` - cycles: `codegraph cycles --sort priority` diff --git a/docs/benchmarks/README.md b/docs/benchmarks/README.md index c8221099..d18b8c89 100644 --- a/docs/benchmarks/README.md +++ b/docs/benchmarks/README.md @@ -4,8 +4,8 @@ On these tiny local fixtures, with Codegraph caching disabled and a fresh process for each sample: -- Direct reads are far faster: their median wall times are tens of milliseconds, while the Codegraph runs take several seconds. -- Codegraph reduces each declared workflow from three calls to one `explore` call. +- The preselected-read baseline completes in the low single-digit milliseconds. It is a filesystem floor with known paths, not a competing repository-discovery workflow. +- Codegraph uses one declared `explore` step while the baseline uses three declared read steps. This count does not represent equivalent work, agent round trips, or tool quality. - Every expected evidence anchor is present in every checked run. These findings describe only the declared workflows and checked fixtures below. Completeness means that expected path anchors appear in captured evidence. It does not measure answer quality, reasoning, relevance, correctness, or whether an agent could produce a good final answer. The benchmark also does not show that either workflow is generally faster, cheaper, or better at repository discovery. @@ -18,7 +18,7 @@ From the repository root, run: npm run bench:docs ``` -This command rebuilds stale `dist` output when needed, runs every scenario and both variants serially, requires complete anchor evidence, writes `.tmp/public-docs-benchmark-results.json`, and prints the median table. The fixtures are local and the run makes no network requests. +This command rebuilds stale `dist` output when needed, runs every scenario and both variants serially, requires complete anchor evidence, rewrites `results.example.json` and the generated table below, and prints the median table. The fixtures are local and the run makes no network requests. ## Workflows and metrics @@ -41,19 +41,29 @@ Medians are calculated independently for each scenario and variant. Tool calls a | Scenario | Variant | Samples | Median tool calls | Median file reads | Median wall time (ms) | Complete runs | Minimum completeness | | -------------------------------- | --------- | ------: | ----------------: | ----------------: | --------------------: | ------------: | -------------------: | -| repo-orientation-small-ts | baseline | 3 | 3 | 3 | 60.458 | 3 | 100% | -| repo-orientation-small-ts | codegraph | 3 | 1 | 3 | 3762.148 | 3 | 100% | -| python-import-reference | baseline | 3 | 3 | 3 | 27.293 | 3 | 100% | -| python-import-reference | codegraph | 3 | 1 | 2 | 3620.385 | 3 | 100% | -| sql-migration-application-review | baseline | 3 | 3 | 3 | 39.783 | 3 | 100% | -| sql-migration-application-review | codegraph | 3 | 1 | 3 | 3569.998 | 3 | 100% | -| mixed-docs-source-graph | baseline | 3 | 3 | 3 | 39.589 | 3 | 100% | -| mixed-docs-source-graph | codegraph | 3 | 1 | 3 | 3437.486 | 3 | 100% | +| repo-orientation-small-ts | baseline | 3 | 3 | 3 | 2.571 | 3 | 100% | +| repo-orientation-small-ts | codegraph | 3 | 1 | 3 | 490.863 | 3 | 100% | +| python-import-reference | baseline | 3 | 3 | 3 | 2.068 | 3 | 100% | +| python-import-reference | codegraph | 3 | 1 | 2 | 480.674 | 3 | 100% | +| sql-migration-application-review | baseline | 3 | 3 | 3 | 2.09 | 3 | 100% | +| sql-migration-application-review | codegraph | 3 | 1 | 3 | 522.643 | 3 | 100% | +| mixed-docs-source-graph | baseline | 3 | 3 | 3 | 2.164 | 3 | 100% | +| mixed-docs-source-graph | codegraph | 3 | 1 | 3 | 461.371 | 3 | 100% | The checked result document records Node, platform, architecture, CPU, logical CPU count, and memory so reruns can be compared in context. +## Where the checked latency comes from + +The checked artifact was produced from a Windows checkout. Its environment metadata is recorded in `results.example.json`, and the generated table above is the source of its measured values. + +The comparison is intentionally end-to-end but not process-symmetric. Baseline reads execute inside the already-running harness and read three preselected files; Codegraph launches a fresh Node process, discovers files, builds a cold index, searches, constructs evidence packets, and serializes JSON. The table measures workflow latency and call count, not equivalent-operation throughput or native parser speed. + +Read the Codegraph wall-time rows as absolute cold CLI latency. Do not divide them by the baseline rows to estimate a slowdown; use an equal-work engine benchmark for parser or index throughput comparisons. + +Persistent MCP/server sessions amortize process startup and project loading. Measure those warm workflows separately before using this cold CLI table to set interactive latency targets. + ## Limitations and variability - These fixtures are tiny, local, synthetic, and network-free. They do not represent large repositories, remote tools, warm indexes, long sessions, concurrent agents, or ambiguous discovery tasks. diff --git a/docs/benchmarks/results.example.json b/docs/benchmarks/results.example.json index 28d613ba..d470d22b 100644 --- a/docs/benchmarks/results.example.json +++ b/docs/benchmarks/results.example.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "generatedAt": "2026-07-10T17:51:31.958Z", + "generatedAt": "2026-07-11T15:36:03.729Z", "command": [ "node", "scripts/benchmarks/run-scenario.mjs", @@ -9,16 +9,16 @@ "--runs", "3", "--output", - ".tmp/public-docs-benchmark-results.json", + "docs/benchmarks/results.example.json", "--require-complete" ], "environment": { "nodeVersion": "v24.15.0", - "platform": "linux", + "platform": "win32", "arch": "x64", "cpuModel": "Intel(R) Core(TM) i9-14900K", "logicalCpus": 32, - "totalMemoryBytes": 67280605184 + "totalMemoryBytes": 137141977088 }, "scenarioFile": "docs/benchmarks/scenarios.json", "scenarioDigest": "sha256:7d1d6583adf2f3cbaf904cae40837c8e25ca1d540cd1b5a28e444d3fc4db67fa", @@ -37,7 +37,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 88.527 + "wallTimeMs": 3.282 }, "checks": { "anchorsExpected": 3, @@ -53,7 +53,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 60.458 + "wallTimeMs": 2.315 }, "checks": { "anchorsExpected": 3, @@ -69,7 +69,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 44.596 + "wallTimeMs": 2.571 }, "checks": { "anchorsExpected": 3, @@ -85,7 +85,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3725.869 + "wallTimeMs": 601.005 }, "checks": { "anchorsExpected": 3, @@ -101,7 +101,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3762.148 + "wallTimeMs": 465.454 }, "checks": { "anchorsExpected": 3, @@ -117,7 +117,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3768.807 + "wallTimeMs": 490.863 }, "checks": { "anchorsExpected": 3, @@ -133,7 +133,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 28.868 + "wallTimeMs": 2.28 }, "checks": { "anchorsExpected": 3, @@ -149,7 +149,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 27.293 + "wallTimeMs": 1.927 }, "checks": { "anchorsExpected": 3, @@ -165,7 +165,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 21.963 + "wallTimeMs": 2.068 }, "checks": { "anchorsExpected": 3, @@ -181,7 +181,7 @@ "metrics": { "toolCalls": 1, "fileReads": 2, - "wallTimeMs": 3474.61 + "wallTimeMs": 480.674 }, "checks": { "anchorsExpected": 3, @@ -197,7 +197,7 @@ "metrics": { "toolCalls": 1, "fileReads": 2, - "wallTimeMs": 3620.385 + "wallTimeMs": 507.044 }, "checks": { "anchorsExpected": 3, @@ -213,7 +213,7 @@ "metrics": { "toolCalls": 1, "fileReads": 2, - "wallTimeMs": 3934.762 + "wallTimeMs": 470.584 }, "checks": { "anchorsExpected": 3, @@ -229,7 +229,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 37.275 + "wallTimeMs": 2.62 }, "checks": { "anchorsExpected": 3, @@ -245,7 +245,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 40.696 + "wallTimeMs": 1.956 }, "checks": { "anchorsExpected": 3, @@ -261,7 +261,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 39.783 + "wallTimeMs": 2.09 }, "checks": { "anchorsExpected": 3, @@ -277,7 +277,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3569.998 + "wallTimeMs": 522.643 }, "checks": { "anchorsExpected": 3, @@ -293,7 +293,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3544.761 + "wallTimeMs": 515.229 }, "checks": { "anchorsExpected": 3, @@ -309,7 +309,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3721.044 + "wallTimeMs": 555.258 }, "checks": { "anchorsExpected": 3, @@ -325,7 +325,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 39.589 + "wallTimeMs": 2.259 }, "checks": { "anchorsExpected": 3, @@ -341,7 +341,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 40.26 + "wallTimeMs": 1.907 }, "checks": { "anchorsExpected": 3, @@ -357,7 +357,7 @@ "metrics": { "toolCalls": 3, "fileReads": 3, - "wallTimeMs": 39.422 + "wallTimeMs": 2.164 }, "checks": { "anchorsExpected": 3, @@ -373,7 +373,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3946.935 + "wallTimeMs": 462.271 }, "checks": { "anchorsExpected": 3, @@ -389,7 +389,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3437.486 + "wallTimeMs": 453.648 }, "checks": { "anchorsExpected": 3, @@ -405,7 +405,7 @@ "metrics": { "toolCalls": 1, "fileReads": 3, - "wallTimeMs": 3410.832 + "wallTimeMs": 461.371 }, "checks": { "anchorsExpected": 3, diff --git a/docs/cli.md b/docs/cli.md index d319c22d..243f647a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -113,7 +113,7 @@ codegraph index --report codegraph review --report --report-file review.report.json ``` -`inspect` emits bounded hotspots, unresolved imports, cycles, and high-confidence duplicate opportunities from a bounded duplicate-analysis pass. Duplicate opportunities are intentionally compact and include file ranges, confidence, clone type, score, token counts, and raw pair counts; run the recommended `duplicates` command for full grouped JSON. +`inspect` emits bounded hotspots, unresolved imports, and cycles. Add `--duplicates` to include a bounded high-confidence duplicate summary; run the recommended `duplicates` command for full grouped JSON. Graph, index, and review reports include `backend.native.byLanguage` so native usage and fallback remain visible per language. Build reports also include `backend.parser` when syntax-tree backend degradation leaves files without parser context. Reports also include `graph.fallbackImportExtraction.byLanguage` and `byReason` when regex import extraction is used. Review JSON reports `diagnostics.symbolMappingParseFailures`, `diagnostics.missingFiles`, `changedFiles[].status` as `updated`, `deleted`, or `missing`, and `sqlContext` when changed SQL files or changed SQL literals make SQL artifact facts relevant. diff --git a/package.json b/package.json index 56edaedc..101c1e45 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "bench:native": "node ./scripts/bench-native.mjs", "bench:native:smoke": "node ./scripts/bench-native.mjs --runs 1 --fixtures typescript,python,go,rust,mixed --workloads full --temperatures cold --max-slowdown 4", "bench:native:full": "node ./scripts/bench-native.mjs --runs 1 --fixtures typescript,python,go,rust,mixed --workloads full,graph --temperatures cold", - "bench:docs": "node ./scripts/ensure-dist-for-tests.mjs && node ./scripts/benchmarks/run-scenario.mjs --output .tmp/public-docs-benchmark-results.json --require-complete && node ./scripts/benchmarks/summarize-results.mjs --input .tmp/public-docs-benchmark-results.json", + "bench:docs": "node ./scripts/ensure-dist-for-tests.mjs && node ./scripts/benchmarks/run-scenario.mjs --output docs/benchmarks/results.example.json --require-complete && node ./scripts/benchmarks/summarize-results.mjs --input docs/benchmarks/results.example.json --scenario-file docs/benchmarks/scenarios.json --readme docs/benchmarks/README.md --write", "bench:docs:check": "node ./scripts/benchmarks/summarize-results.mjs --input docs/benchmarks/results.example.json --scenario-file docs/benchmarks/scenarios.json --readme docs/benchmarks/README.md --check", "test:ui": "node ./scripts/ensure-dist-for-tests.mjs && vitest --ui", "test:run": "node ./scripts/ensure-dist-for-tests.mjs && vitest run", diff --git a/src/agent/session.ts b/src/agent/session.ts index 60eedba3..715380ac 100644 --- a/src/agent/session.ts +++ b/src/agent/session.ts @@ -68,7 +68,7 @@ const EMPTY_SYMBOL_GRAPH: SymbolGraph = { nodes: new Map(), edges: [], }; -const AGENT_NATIVE_WORKER_AUTO_FILE_THRESHOLD = 250; +export const NATIVE_WORKER_AUTO_FILE_THRESHOLD = 250; const DEFAULT_MAX_AUTO_REFRESH_FILES = 50; const DEFAULT_MAX_AUTO_REFRESH_BYTES = 2_000_000; const DEFAULT_MAX_FRESHNESS_CHANGED_FILES = 25; @@ -213,10 +213,7 @@ export function createAgentSession(options: AgentSessionOptions): AgentSession { files, ...(discoveryOptions ? { discovery: discoveryOptions } : {}), }; - if ( - options.buildOptions?.useNativeWorkers === undefined && - files.length >= AGENT_NATIVE_WORKER_AUTO_FILE_THRESHOLD - ) { + if (options.buildOptions?.useNativeWorkers === undefined && files.length >= NATIVE_WORKER_AUTO_FILE_THRESHOLD) { buildOptions.useNativeWorkers = true; } const buildReport: BuildReport = { timings: {} }; diff --git a/src/cli/help.ts b/src/cli/help.ts index 4d395d19..d1ee9dac 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -64,6 +64,7 @@ Build Options: Analysis Output Options: --json Output analysis commands as JSON (default where supported) + --duplicates Include duplicate summary in inspect (off by default) --mermaid Output as Mermaid diagram --dot Output as DOT graph --sqlite Write to SQLite database @@ -100,6 +101,7 @@ Examples: codegraph install --print-config codex codegraph uninstall --target codex --yes codegraph inspect ./src --limit 20 + codegraph inspect ./src --limit 20 --duplicates codegraph duplicates ./src --min-confidence medium codegraph graph ./src codegraph graph --fast-graph --mermaid ./src diff --git a/src/cli/inspect.ts b/src/cli/inspect.ts index b335b586..c9272e89 100644 --- a/src/cli/inspect.ts +++ b/src/cli/inspect.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import path from "node:path"; +import { NATIVE_WORKER_AUTO_FILE_THRESHOLD } from "../agent/session.js"; import { findDuplicates, type DuplicateConfidence, type DuplicateGroup } from "../duplicates.js"; import { collectGraph } from "../graph-builder.js"; import { findDetailedCycles, getUnresolvedImports } from "../graphs/queries.js"; @@ -63,12 +64,15 @@ type InspectReport = { size: number; }>; }; - duplicates: { - total: number; - omitted: number; - minConfidence: DuplicateConfidence; - top: DuplicateOpportunitySummary[]; - }; + duplicates: + | { enabled: false } + | { + enabled: true; + total: number; + omitted: number; + minConfidence: DuplicateConfidence; + top: DuplicateOpportunitySummary[]; + }; recommendedCommands: string[]; }; @@ -253,6 +257,7 @@ async function buildInspectReport( workerOpts: { useNativeWorkers: true } | Record, progressHandler: ((update: { current: number; total: number }) => void) | undefined, limit: number, + includeDuplicates: boolean, writeStderrLine: (message: string) => void, ): Promise { const useDiskCache = cache === "disk" || cache === undefined; @@ -260,29 +265,43 @@ async function buildInspectReport( if (indexCache) { writeStderrLine(formatIndexCacheMetadata(indexCache)); } + const useNativeWorkers = "useNativeWorkers" in workerOpts || files.length >= NATIVE_WORKER_AUTO_FILE_THRESHOLD; const index = await buildProjectIndexIncremental(projectRoot, { files, cache: cache ?? "disk", discovery, ...(progressHandler ? { onProgress: progressHandler } : {}), ...(nativeMode !== "auto" ? { native: nativeMode } : {}), - ...workerOpts, + ...(useNativeWorkers ? { useNativeWorkers: true } : {}), ...(graphOptions ? { graph: graphOptions } : {}), }); const graph = restrictGraphToIncludeRoots(index.graph, includeRoots, normalizePathForDisplay); const hotspots = getHotspots(graph, { limit }); const unresolved = getUnresolvedImports(graph, { projectRoot }); const cycles = findDetailedCycles(graph); - const duplicateMinConfidence: DuplicateConfidence = "high"; - const duplicateResult = await findDuplicates(index, { - projectRoot, - files, - includeSameFile: true, - minConfidence: duplicateMinConfidence, - minTokens: INSPECT_DUPLICATE_MIN_TOKENS, - maxPairs: INSPECT_DUPLICATE_MAX_PAIRS, - limit, - }); + let duplicates: InspectReport["duplicates"] = { enabled: false }; + if (includeDuplicates) { + const minConfidence: DuplicateConfidence = "high"; + const duplicateResult = await findDuplicates(index, { + projectRoot, + files, + includeSameFile: true, + minConfidence, + minTokens: INSPECT_DUPLICATE_MIN_TOKENS, + maxPairs: INSPECT_DUPLICATE_MAX_PAIRS, + limit, + }); + duplicates = { + enabled: true, + total: + duplicateResult.groups.length + + duplicateResult.omittedCounts.groups + + duplicateResult.omittedCounts.candidatePairs, + omitted: duplicateResult.omittedCounts.groups + duplicateResult.omittedCounts.candidatePairs, + minConfidence, + top: duplicateResult.groups.map(summarizeDuplicateGroup), + }; + } const loadError = getNativeTreeSitterLoadError(nativeMode); return { root: normalizePathForDisplay(projectRoot), @@ -315,15 +334,7 @@ async function buildInspectReport( size: cycle.files.length, })), }, - duplicates: { - total: - duplicateResult.groups.length + - duplicateResult.omittedCounts.groups + - duplicateResult.omittedCounts.candidatePairs, - omitted: duplicateResult.omittedCounts.groups + duplicateResult.omittedCounts.candidatePairs, - minConfidence: duplicateMinConfidence, - top: duplicateResult.groups.map(summarizeDuplicateGroup), - }, + duplicates, recommendedCommands: buildRecommendedInspectCommands( projectRoot, includeRoots, @@ -348,6 +359,7 @@ export async function handleInspectCommand(context: InspectCommandContext): Prom context.workerOpts, context.progressHandler, limit, + context.hasFlag("--duplicates"), context.writeStderrLine, ); context.writeJSONLine(report); diff --git a/src/cli/options.ts b/src/cli/options.ts index a0c1f7ee..bfa34b62 100644 --- a/src/cli/options.ts +++ b/src/cli/options.ts @@ -88,9 +88,7 @@ const CLI_VALUE_OPTIONS = new Set([ ]); type CliPositionalPolicy = - | { kind: "any" } - | { kind: "max"; max: number; usage: string } - | { kind: "none"; usage: string }; + { kind: "any" } | { kind: "max"; max: number; usage: string } | { kind: "none"; usage: string }; type CliCommandSchema = { flags?: readonly string[]; @@ -415,7 +413,9 @@ const CLI_COMMAND_SCHEMAS = new Map([ ], [ "inspect", - commandSchema([...SHARED_BUILD_FLAGS, ...JSON_OUTPUT_FLAGS], [...SHARED_BUILD_OPTIONS, "--limit"], { kind: "any" }), + commandSchema([...SHARED_BUILD_FLAGS, ...JSON_OUTPUT_FLAGS, "--duplicates"], [...SHARED_BUILD_OPTIONS, "--limit"], { + kind: "any", + }), ], [ "mcp", @@ -602,6 +602,7 @@ export function validateCliArgs(command: string, parsed: ParsedCliArgs): void { export function isCliValueOption(command: string, key: string, positionals: readonly string[]): boolean { if (command === "artifact" && key === "--sqlite" && positionals[0] === "build") return false; + if (command === "inspect" && key === "--duplicates") return false; return CLI_VALUE_OPTIONS.has(key); } diff --git a/tests/cli-command-modules.test.ts b/tests/cli-command-modules.test.ts index f579cbfe..df86efe3 100644 --- a/tests/cli-command-modules.test.ts +++ b/tests/cli-command-modules.test.ts @@ -12,6 +12,7 @@ import { handleGraphDeltaCommand } from "../src/cli/graphDelta.js"; import { handleGraphQueryCommand, type GraphQueryCommandContext } from "../src/cli/graphQueries.js"; import { CLI_HELP_TEXT, FILE_HELP_TEXT, MCP_SERVE_HELP_TEXT, PACKET_HELP_TEXT } from "../src/cli/help.js"; import { handleImpactCommand, type ImpactCommandContext } from "../src/cli/impact.js"; +import { handleInspectCommand, type InspectCommandContext } from "../src/cli/inspect.js"; import { handleGotoCommand, type NavigationCommandContext } from "../src/cli/navigation.js"; import { getCodegraphPackageIdentity, getCodegraphVersion } from "../src/cli/packageInfo.js"; import { handlePacketCommand } from "../src/cli/packet.js"; @@ -111,6 +112,26 @@ function createNavigationContext(overrides: Partial): }; } +function createInspectContext(overrides: Partial): InspectCommandContext { + const projectRoot = path.join(os.tmpdir(), "codegraph-inspect-context").replace(/\\/g, "/"); + return { + projectRootFs: projectRoot, + includeRootsAbs: [projectRoot], + discoveryOptions: {}, + graphOptions: undefined, + nativeMode: "auto", + workerOpts: {}, + progressHandler: undefined, + getOpt: (name) => (name === "--cache" ? "off" : undefined), + hasFlag: () => false, + resolveFilesFromRoots: async () => [], + writeJSONLine: () => undefined, + writeStdoutLine: () => undefined, + writeStderrLine: () => undefined, + ...overrides, + }; +} + function createGraphContext(overrides: Partial): GraphCommandContext { const projectRoot = path.join(os.tmpdir(), "codegraph-graph-context").replace(/\\/g, "/"); return { @@ -829,6 +850,29 @@ describe("CLI command modules", () => { } }); + test("inspect auto-enables native workers at the agent-session file threshold", async () => { + const emptyIndex: ProjectIndex = { + graph: { nodes: new Set(), edges: [] }, + modules: new Map(), + byFile: new Map(), + exportCache: new Map(), + scopeCache: new Map(), + }; + const buildSpy = vi.spyOn(indexerBuild, "buildProjectIndexIncremental").mockResolvedValue(emptyIndex); + const largeFiles = Array.from({ length: 250 }, (_, index) => `file-${index}.ts`); + const smallFiles = largeFiles.slice(1); + + try { + await handleInspectCommand(createInspectContext({ resolveFilesFromRoots: async () => smallFiles })); + await handleInspectCommand(createInspectContext({ resolveFilesFromRoots: async () => largeFiles })); + + expect(buildSpy.mock.calls[0]?.[1]?.useNativeWorkers).toBeUndefined(); + expect(buildSpy.mock.calls[1]?.[1]?.useNativeWorkers).toBe(true); + } finally { + buildSpy.mockRestore(); + } + }); + test("uses shared cache-mode validation in the extracted graph-delta command handler", async () => { await expect( handleGraphDeltaCommand({ diff --git a/tests/cli-regressions.test.ts b/tests/cli-regressions.test.ts index 0fd02c6c..df1afca4 100644 --- a/tests/cli-regressions.test.ts +++ b/tests/cli-regressions.test.ts @@ -905,18 +905,7 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) hotspots: Array<{ file: string; fanIn: number; fanOut: number; score: number }>; unresolved: { total: number; top: Array<{ name: string; importerCount: number }> }; cycles: { total: number; top: Array<{ files: string[]; priorityScore: number; size: number }> }; - duplicates: { - total: number; - omitted: number; - minConfidence: string; - top: Array<{ - confidence: string; - cloneType: string; - left: { file: string; startLine: number; endLine: number; tokenCount: number; name?: string }; - right: { file: string; startLine: number; endLine: number; tokenCount: number; name?: string }; - rawPairCount: number; - }>; - }; + duplicates: { enabled: false }; recommendedCommands: string[]; }; @@ -932,16 +921,35 @@ export function summarizeInvoices(rows: Array<{ amount: number; tax: number }>) expect(report.unresolved.top).toEqual([]); expect(report.cycles.total).toBe(0); expect(report.cycles.top).toEqual([]); - expect(report.duplicates.total).toBeGreaterThan(0); - expect(report.duplicates.omitted).toBeGreaterThanOrEqual(0); - expect(report.duplicates.minConfidence).toBe("high"); - expect(report.duplicates.top).toHaveLength(1); - expect(report.duplicates.top[0]?.confidence).toBe("high"); - expect(report.duplicates.top[0]?.cloneType).toBe("exact"); - expect(report.duplicates.top[0]?.left.file).toBe("src/c.ts"); - expect(report.duplicates.top[0]?.right.file).toBe("src/d.ts"); - expect(report.duplicates.top[0]?.left.tokenCount).toBeGreaterThan(40); - expect(report.duplicates.top[0]?.rawPairCount).toBeGreaterThan(0); + expect(report.duplicates).toEqual({ enabled: false }); + + const duplicatesStdout = await runCliCommand(["inspect", "--duplicates", "--root", tmpDir, srcDir, "--limit", "1"]); + const duplicatesReport = JSON.parse(duplicatesStdout) as { + duplicates: { + enabled: true; + total: number; + omitted: number; + minConfidence: string; + top: Array<{ + confidence: string; + cloneType: string; + left: { file: string; startLine: number; endLine: number; tokenCount: number; name?: string }; + right: { file: string; startLine: number; endLine: number; tokenCount: number; name?: string }; + rawPairCount: number; + }>; + }; + }; + expect(duplicatesReport.duplicates.enabled).toBe(true); + expect(duplicatesReport.duplicates.total).toBeGreaterThan(0); + expect(duplicatesReport.duplicates.omitted).toBeGreaterThanOrEqual(0); + expect(duplicatesReport.duplicates.minConfidence).toBe("high"); + expect(duplicatesReport.duplicates.top).toHaveLength(1); + expect(duplicatesReport.duplicates.top[0]?.confidence).toBe("high"); + expect(duplicatesReport.duplicates.top[0]?.cloneType).toBe("exact"); + expect(duplicatesReport.duplicates.top[0]?.left.file).toBe("src/c.ts"); + expect(duplicatesReport.duplicates.top[0]?.right.file).toBe("src/d.ts"); + expect(duplicatesReport.duplicates.top[0]?.left.tokenCount).toBeGreaterThan(40); + expect(duplicatesReport.duplicates.top[0]?.rawPairCount).toBeGreaterThan(0); expect(report.recommendedCommands).toContain( `codegraph hotspots --root "${normalize(tmpDir)}" "${normalize(srcDir)}" --limit 20 --json`, );