diff --git a/src/cli-map.ts b/src/cli-map.ts index 0f13606..4479afe 100644 --- a/src/cli-map.ts +++ b/src/cli-map.ts @@ -32,7 +32,8 @@ function printMapUsage(): void { console.log('Output the conventions map for the current codebase.'); console.log(''); console.log('Options:'); - console.log(' --export Write CODEBASE_MAP.md to project root (overrides other flags)'); + console.log(' --export Write CODEBASE_MAP.md to project root (still honors --full)'); + console.log(' --full Output the exhaustive map instead of the bounded default'); console.log(' --json Output raw JSON (CodebaseMapSummary)'); console.log(' --pretty Terminal-friendly box layout'); console.log(' --help Show this help'); @@ -44,6 +45,7 @@ export async function handleMapCli(args: string[]): Promise { const useJson = args.includes('--json'); const usePretty = args.includes('--pretty'); const useExport = args.includes('--export'); + const useFull = args.includes('--full'); const showHelp = args.includes('--help') || args.includes('-h'); if (showHelp) { @@ -77,7 +79,7 @@ export async function handleMapCli(args: string[]): Promise { project.indexState = indexState; try { - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: useFull ? 'full' : 'bounded' }); if (useExport) { const outPath = path.join(rootPath, 'CODEBASE_MAP.md'); diff --git a/src/core/codebase-map.ts b/src/core/codebase-map.ts index 90a8487..7a2c4d6 100644 --- a/src/core/codebase-map.ts +++ b/src/core/codebase-map.ts @@ -24,7 +24,11 @@ import type { PatternsData, CodeChunk } from '../types/index.js'; -import { RELATIONSHIPS_FILENAME, KEYWORD_INDEX_FILENAME } from '../constants/codebase-context.js'; +import { + EXCLUDED_DIRECTORY_NAMES, + RELATIONSHIPS_FILENAME, + KEYWORD_INDEX_FILENAME +} from '../constants/codebase-context.js'; // --------------------------------------------------------------------------- // Internal types for relationships.json @@ -50,12 +54,37 @@ interface RelationshipsData { }; } -// --------------------------------------------------------------------------- -// Entrypoint exclusion pattern -// --------------------------------------------------------------------------- - -const ENTRYPOINT_EXCLUSION_RE = - /(?:^|\/)(?:tests?|__tests__|fixtures?|scripts?)\/|\.test\.|\.spec\./; +type CodebaseMapMode = 'bounded' | 'full'; + +type BuildCodebaseMapOptions = { + mode?: CodebaseMapMode; +}; + +const BOUNDED_SECTION_LIMITS = { + entrypoints: 8, + hubFiles: 5, + keyInterfaces: 8, + apiSurfaceFiles: 8, + apiSurfaceExports: 3, + hotspots: 5, + bestExamples: 3 +} as const; + +const MAP_EXCLUDED_PATH_PATTERNS = [ + /^repos\/[^/]+(?:\/|$)/i, + /(?:^|\/)(?:tests?|__tests__|specs?|__specs__)(?:\/|$)/i, + /\.(?:test|spec)\.[^/]+$/i, + /(?:^|\/)(?:fixtures?|__fixtures__)(?:\/|$)/i, + /(?:^|\/)(?:generated|__generated__)(?:\/|$)/i, + /(?:^|\/)[^/]*\.(?:generated|gen|min)\.[^/]+$/i, + /\.snap$/i +] as const; + +const MAP_EXCLUDED_DIRECTORY_NAMES = new Set( + [...EXCLUDED_DIRECTORY_NAMES, '__fixtures__', '__generated__', 'fixtures', 'generated'].map( + (segment) => segment.toLowerCase() + ) +); // --------------------------------------------------------------------------- // Builder @@ -66,7 +95,11 @@ const ENTRYPOINT_EXCLUSION_RE = * Reads `intelligence.json`, `relationships.json`, and `index.json` from project paths. * Degrades gracefully when artifacts are missing. */ -export async function buildCodebaseMap(project: ProjectState): Promise { +export async function buildCodebaseMap( + project: ProjectState, + options: BuildCodebaseMapOptions = {} +): Promise { + const mode = options.mode ?? 'bounded'; const projectName = path.basename(project.rootPath); // Read intelligence.json @@ -100,9 +133,10 @@ export async function buildCodebaseMap(project: ProjectState): Promise isMapEligiblePath(chunk.relativePath, mode)); // relationships.json has stats at top level OR inside graph const statsSource = relationships.stats ?? @@ -133,13 +167,13 @@ export async function buildCodebaseMap(project: ProjectState): Promise = Object.entries( @@ -150,17 +184,17 @@ export async function buildCodebaseMap(project: ProjectState): Promise x.count, (x) => x.file ) - .slice(0, 5) + .slice(0, mode === 'bounded' ? BOUNDED_SECTION_LIMITS.hubFiles : undefined) .map((x) => x.file); // --- Key interfaces --- - const keyInterfaces = deriveKeyInterfaces(chunks, graphImportedBy); + const keyInterfaces = deriveKeyInterfaces(filteredChunks, graphImportedBy, mode); // --- API surface --- - const apiSurface = deriveApiSurface(entrypoints, graphExports); + const apiSurface = deriveApiSurface(boundedEntrypoints, graphExports, mode); // --- Dependency hotspots --- - const hotspots = deriveHotspots(graphImports, graphImportedBy); + const hotspots = deriveHotspots(graphImports, graphImportedBy, mode); // --- Active patterns --- const patterns: PatternsData = intelligence.patterns ?? {}; @@ -187,8 +221,12 @@ export async function buildCodebaseMap(project: ProjectState): Promise 0 ? activePatterns[0].name : 'high-quality example'; - const goldenFiles = intelligence.goldenFiles ?? []; - const bestExamples: CodebaseMapExample[] = goldenFiles.slice(0, 3).map((gf) => ({ + const goldenFiles = (intelligence.goldenFiles ?? []).filter((gf) => isMapEligiblePath(gf.file, mode)); + const bestExamples: CodebaseMapExample[] = maybeLimit( + goldenFiles, + BOUNDED_SECTION_LIMITS.bestExamples, + mode + ).map((gf) => ({ file: gf.file, score: gf.score, reason: dominantPatternName @@ -210,7 +248,14 @@ export async function buildCodebaseMap(project: ProjectState): Promise + graphImportedBy: Record, + mode: CodebaseMapMode ): CodebaseMapKeyInterface[] { const symbolChunks = chunks.filter( (c) => c.metadata?.symbolAware === true && SYMBOL_KINDS.has(c.metadata.symbolKind ?? '') @@ -251,18 +297,21 @@ function deriveKeyInterfaces( if (lenDiff !== 0) return lenDiff; return a.chunk.relativePath.localeCompare(b.chunk.relativePath); }); - return scored.slice(0, 10).map(({ chunk, importerCount }) => ({ - name: chunk.metadata.symbolName ?? path.basename(chunk.relativePath), - kind: chunk.metadata.symbolKind ?? 'unknown', - file: chunk.relativePath, - importerCount, - signatureHint: buildSignatureHint(chunk.content) - })); + return maybeLimit(scored, BOUNDED_SECTION_LIMITS.keyInterfaces, mode).map( + ({ chunk, importerCount }) => ({ + name: chunk.metadata.symbolName ?? path.basename(chunk.relativePath), + kind: chunk.metadata.symbolKind ?? 'unknown', + file: chunk.relativePath, + importerCount, + signatureHint: buildSignatureHint(chunk.content) + }) + ); } function deriveApiSurface( entrypoints: string[], - graphExports: Record> + graphExports: Record>, + mode: CodebaseMapMode ): CodebaseMapApiSurface[] { const results: CodebaseMapApiSurface[] = []; for (const ep of entrypoints) { @@ -271,16 +320,17 @@ function deriveApiSurface( const names = exps .map((e) => e.name) .filter((n) => n && n !== 'default') - .slice(0, 5); + .slice(0, mode === 'bounded' ? BOUNDED_SECTION_LIMITS.apiSurfaceExports : undefined); if (names.length === 0) continue; results.push({ file: ep, exports: names }); } - return results; + return maybeLimit(results, BOUNDED_SECTION_LIMITS.apiSurfaceFiles, mode); } function deriveHotspots( graphImports: Record, - graphImportedBy: Record + graphImportedBy: Record, + mode: CodebaseMapMode ): CodebaseMapHotspot[] { const allFiles = new Set([...Object.keys(graphImports), ...Object.keys(graphImportedBy)]); const hotspots: CodebaseMapHotspot[] = []; @@ -295,7 +345,7 @@ function deriveHotspots( if (b.combined !== a.combined) return b.combined - a.combined; return a.file.localeCompare(b.file); }); - return hotspots.slice(0, 5); + return maybeLimit(hotspots, BOUNDED_SECTION_LIMITS.hotspots, mode); } function enrichLayers( @@ -642,3 +692,61 @@ function sortByCountThenAlpha( return getName(a).localeCompare(getName(b)); }); } + +function maybeLimit(items: T[], limit: number, mode: CodebaseMapMode): T[] { + return mode === 'bounded' ? items.slice(0, limit) : items; +} + +function filterAdjacencyGraph( + graph: Record, + mode: CodebaseMapMode +): Record { + if (mode === 'full') { + return graph; + } + + return Object.fromEntries( + Object.entries(graph) + .filter(([file]) => isMapEligiblePath(file, mode)) + .map(([file, related]) => [file, related.filter((item) => isMapEligiblePath(item, mode))]) + ); +} + +function filterExportGraph( + graph: Record>, + mode: CodebaseMapMode +): Record> { + if (mode === 'full') { + return graph; + } + + return Object.fromEntries( + Object.entries(graph).filter(([file]) => isMapEligiblePath(file, mode)) + ); +} + +function isMapEligiblePath(filePath: string, mode: CodebaseMapMode): boolean { + if (mode === 'full') { + return true; + } + + const normalizedPath = normalizeMapPath(filePath); + if (!normalizedPath) { + return false; + } + + const segments = normalizedPath + .split('/') + .map((segment) => segment.toLowerCase()) + .filter(Boolean); + + if (segments.some((segment) => MAP_EXCLUDED_DIRECTORY_NAMES.has(segment))) { + return false; + } + + return !MAP_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath)); +} + +function normalizeMapPath(filePath: string): string { + return filePath.replace(/\\/g, '/').replace(/^\.\//, '').trim(); +} diff --git a/src/index.ts b/src/index.ts index a4d7c73..52877d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,11 +37,18 @@ import { startFileWatcher } from './core/file-watcher.js'; import { parseGitLogLineToMemory } from './memory/git-memory.js'; import { CONTEXT_RESOURCE_URI, + FULL_CONTEXT_RESOURCE_URI, buildProjectContextResourceUri, + buildProjectFullContextResourceUri, getProjectPathFromContextResourceUri, - isContextResourceUri + getProjectPathFromFullContextResourceUri, + isContextResourceUri, + isFullContextResourceUri } from './resources/uri.js'; -import { generateCodebaseIntelligence } from './resources/codebase-intelligence.js'; +import { + generateCodebaseIntelligence, + generateFullCodebaseIntelligence +} from './resources/codebase-intelligence.js'; import { EXCLUDED_GLOB_PATTERNS } from './constants/codebase-context.js'; import { discoverProjectsWithinRoot, @@ -846,6 +853,27 @@ export function registerHandlers(target: Server): void { target.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const explicitProjectPath = getProjectPathFromContextResourceUri(uri); + const explicitFullProjectPath = getProjectPathFromFullContextResourceUri(uri); + + if (explicitFullProjectPath) { + const selection = await resolveProjectSelector(explicitFullProjectPath); + if (!selection.ok) { + throw new Error(`Unknown project resource: ${uri}`); + } + + const project = selection.project; + await initProject(project.rootPath, watcherDebounceMs, { enableWatcher: true }); + setActiveProject(project.rootPath); + return { + contents: [ + { + uri: buildProjectFullContextResourceUri(project.rootPath), + mimeType: 'text/plain', + text: await generateFullCodebaseIntelligence(project) + } + ] + }; + } if (explicitProjectPath) { const selection = await resolveProjectSelector(explicitProjectPath); @@ -867,6 +895,21 @@ export function registerHandlers(target: Server): void { }; } + if (isFullContextResourceUri(uri)) { + const project = await resolveProjectForResource(); + return { + contents: [ + { + uri: FULL_CONTEXT_RESOURCE_URI, + mimeType: 'text/plain', + text: project + ? await generateFullCodebaseIntelligence(project) + : buildProjectSelectionMessage() + } + ] + }; + } + if (isContextResourceUri(uri)) { const project = await resolveProjectForResource(); return { @@ -1010,6 +1053,13 @@ function buildResources(): Resource[] { description: 'Context for the active project in this MCP session. In multi-project sessions, this falls back to a workspace overview until a project is selected.', mimeType: 'text/plain' + }, + { + uri: FULL_CONTEXT_RESOURCE_URI, + name: 'Codebase Intelligence (Full)', + description: + 'Exhaustive conventions map for the active project. Use when you explicitly need the unbounded map instead of the bounded first-call surface.', + mimeType: 'text/plain' } ]; @@ -1020,6 +1070,12 @@ function buildResources(): Resource[] { description: `Project-scoped context for ${project.label}.`, mimeType: 'text/plain' }); + resources.push({ + uri: buildProjectFullContextResourceUri(project.rootPath), + name: `Codebase Intelligence (Full) (${project.label})`, + description: `Exhaustive project-scoped context for ${project.label}.`, + mimeType: 'text/plain' + }); } return resources; @@ -1054,6 +1110,7 @@ function buildProjectSelectionMessage(): string { lines.push(`- ${project.label} [${project.indexStatus}]`); lines.push(` project: ${projectPathHint}`); lines.push(` resource: ${buildProjectContextResourceUri(project.rootPath)}`); + lines.push(` full resource: ${buildProjectFullContextResourceUri(project.rootPath)}`); } lines.push(''); lines.push('Recommended flow: retry the tool call with `project`.'); diff --git a/src/resources/codebase-intelligence.ts b/src/resources/codebase-intelligence.ts index 50463da..e4f5691 100644 --- a/src/resources/codebase-intelligence.ts +++ b/src/resources/codebase-intelligence.ts @@ -10,7 +10,23 @@ import { buildCodebaseMap, renderMapMarkdown } from '../core/codebase-map.js'; */ export async function generateCodebaseIntelligence(project: ProjectState): Promise { try { - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'bounded' }); + return renderMapMarkdown(map); + } catch (error) { + return ( + '# Codebase Intelligence\n\n' + + 'Intelligence data not yet generated. Run indexing first.\n' + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +/** + * Generate the exhaustive conventions-map payload for the explicit full-mode resources. + */ +export async function generateFullCodebaseIntelligence(project: ProjectState): Promise { + try { + const map = await buildCodebaseMap(project, { mode: 'full' }); return renderMapMarkdown(map); } catch (error) { return ( diff --git a/src/resources/uri.ts b/src/resources/uri.ts index dc67d9c..cb9e690 100644 --- a/src/resources/uri.ts +++ b/src/resources/uri.ts @@ -1,14 +1,13 @@ const CONTEXT_RESOURCE_URI = 'codebase://context'; const PROJECT_CONTEXT_RESOURCE_PREFIX = `${CONTEXT_RESOURCE_URI}/project/`; +const FULL_CONTEXT_RESOURCE_URI = `${CONTEXT_RESOURCE_URI}/full`; +const FULL_PROJECT_CONTEXT_RESOURCE_PREFIX = `${FULL_CONTEXT_RESOURCE_URI}/project/`; export function normalizeResourceUri(uri: string): string { if (!uri) return uri; - if (uri === CONTEXT_RESOURCE_URI) return uri; - if (uri.endsWith(`/${CONTEXT_RESOURCE_URI}`)) return CONTEXT_RESOURCE_URI; - const scopedMarker = `/${PROJECT_CONTEXT_RESOURCE_PREFIX}`; - const scopedIndex = uri.indexOf(scopedMarker); - if (scopedIndex >= 0) { - return uri.slice(scopedIndex + 1); + const resourceIndex = uri.indexOf(CONTEXT_RESOURCE_URI); + if (resourceIndex >= 0) { + return uri.slice(resourceIndex); } return uri; } @@ -17,10 +16,18 @@ export function isContextResourceUri(uri: string): boolean { return normalizeResourceUri(uri) === CONTEXT_RESOURCE_URI; } +export function isFullContextResourceUri(uri: string): boolean { + return normalizeResourceUri(uri) === FULL_CONTEXT_RESOURCE_URI; +} + export function buildProjectContextResourceUri(projectPath: string): string { return `${PROJECT_CONTEXT_RESOURCE_PREFIX}${encodeURIComponent(projectPath)}`; } +export function buildProjectFullContextResourceUri(projectPath: string): string { + return `${FULL_PROJECT_CONTEXT_RESOURCE_PREFIX}${encodeURIComponent(projectPath)}`; +} + export function getProjectPathFromContextResourceUri(uri: string): string | undefined { const normalized = normalizeResourceUri(uri); if (!normalized.startsWith(PROJECT_CONTEXT_RESOURCE_PREFIX)) { @@ -31,4 +38,19 @@ export function getProjectPathFromContextResourceUri(uri: string): string | unde return encodedProjectPath ? decodeURIComponent(encodedProjectPath) : undefined; } -export { CONTEXT_RESOURCE_URI, PROJECT_CONTEXT_RESOURCE_PREFIX }; +export function getProjectPathFromFullContextResourceUri(uri: string): string | undefined { + const normalized = normalizeResourceUri(uri); + if (!normalized.startsWith(FULL_PROJECT_CONTEXT_RESOURCE_PREFIX)) { + return undefined; + } + + const encodedProjectPath = normalized.slice(FULL_PROJECT_CONTEXT_RESOURCE_PREFIX.length); + return encodedProjectPath ? decodeURIComponent(encodedProjectPath) : undefined; +} + +export { + CONTEXT_RESOURCE_URI, + PROJECT_CONTEXT_RESOURCE_PREFIX, + FULL_CONTEXT_RESOURCE_URI, + FULL_PROJECT_CONTEXT_RESOURCE_PREFIX +}; diff --git a/tests/__snapshots__/codebase-map.test.ts.snap b/tests/__snapshots__/codebase-map.test.ts.snap index be94e64..588f8bd 100644 --- a/tests/__snapshots__/codebase-map.test.ts.snap +++ b/tests/__snapshots__/codebase-map.test.ts.snap @@ -6,7 +6,6 @@ exports[`renderMapMarkdown > renders deterministic markdown from fixture — sna ## Architecture Layers - **src** (5 files) — hub: \`src/core/search.ts\` -- **tests** (2 files) - **lib** (1 file) — hub: \`lib/utils.ts\` ## Entrypoints @@ -16,19 +15,19 @@ exports[`renderMapMarkdown > renders deterministic markdown from fixture — sna ## Hub Files +- \`lib/utils.ts\` - \`src/core/search.ts\` - \`src/utils/helpers.ts\` -- \`lib/utils.ts\` ## Key Interfaces -- **SearchOptions** \`interface\` — \`src/core/search.ts\` (imported by 3) +- **SearchOptions** \`interface\` — \`src/core/search.ts\` (imported by 2) \`\`\` export interface SearchOptions { query: string; limit?: number; \`\`\` -- **CodebaseSearcher** \`class\` — \`src/core/search.ts\` (imported by 3) +- **CodebaseSearcher** \`class\` — \`src/core/search.ts\` (imported by 2) \`\`\` export class CodebaseSearcher { private rootPath: string; @@ -46,11 +45,11 @@ exports[`renderMapMarkdown > renders deterministic markdown from fixture — sna ## Dependency Hotspots -- \`src/core/search.ts\` — imported by 3, imports 2 (combined: 5) -- \`src/utils/helpers.ts\` — imported by 3, imports 0 (combined: 3) +- \`src/core/search.ts\` — imported by 2, imports 2 (combined: 4) - \`lib/utils.ts\` — imported by 2, imports 0 (combined: 2) - \`src/cli.ts\` — imported by 0, imports 2 (combined: 2) - \`src/index.ts\` — imported by 0, imports 2 (combined: 2) +- \`src/utils/helpers.ts\` — imported by 2, imports 0 (combined: 2) ## Active Patterns diff --git a/tests/codebase-map.test.ts b/tests/codebase-map.test.ts index 6e1465d..0cd1aec 100644 --- a/tests/codebase-map.test.ts +++ b/tests/codebase-map.test.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from 'url'; import { createProjectState } from '../src/project-state.js'; import { buildCodebaseMap, renderMapMarkdown, renderMapPretty } from '../src/core/codebase-map.js'; import { generateCodebaseIntelligence } from '../src/resources/codebase-intelligence.js'; +import type { CodeChunk } from '../src/types/index.js'; import { CODEBASE_CONTEXT_DIRNAME, INTELLIGENCE_FILENAME, @@ -17,6 +18,68 @@ import { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const FIXTURE_ROOT = path.join(__dirname, 'fixtures', 'map-fixture'); +const CURRENT_REPO_ROOT = path.resolve(__dirname, '..'); +const BOUNDED_LIMITS = { + entrypoints: 8, + hubFiles: 5, + keyInterfaces: 8, + apiSurfaceFiles: 8, + apiSurfaceExports: 3, + hotspots: 5, + bestExamples: 3 +} as const; + +type TempGraph = { + imports?: Record; + importedBy?: Record; + exports?: Record>; + stats?: { files?: number; edges?: number; avgDependencies?: number }; +}; + +type TempProjectOptions = { + projectName?: string; + graph?: TempGraph; + goldenFiles?: Array<{ file: string; score: number }>; + patterns?: Record; + chunks?: CodeChunk[]; +}; + +async function createTempMapProject(options: TempProjectOptions = {}): Promise { + const tempParent = await fs.mkdtemp(path.join(os.tmpdir(), 'codebase-map-project-')); + const projectName = options.projectName ?? 'temp-map-project'; + const rootPath = path.join(tempParent, projectName); + const ctxDir = path.join(rootPath, CODEBASE_CONTEXT_DIRNAME); + + await fs.mkdir(ctxDir, { recursive: true }); + await fs.writeFile( + path.join(ctxDir, INTELLIGENCE_FILENAME), + JSON.stringify( + { + patterns: options.patterns ?? {}, + goldenFiles: options.goldenFiles ?? [] + }, + null, + 2 + ), + 'utf-8' + ); + await fs.writeFile( + path.join(ctxDir, KEYWORD_INDEX_FILENAME), + JSON.stringify({ chunks: options.chunks ?? [] }, null, 2), + 'utf-8' + ); + await fs.writeFile( + path.join(ctxDir, RELATIONSHIPS_FILENAME), + JSON.stringify({ graph: options.graph ?? {} }, null, 2), + 'utf-8' + ); + + return rootPath; +} + +async function removeTempMapProject(rootPath: string): Promise { + await fs.rm(path.dirname(rootPath), { recursive: true, force: true }); +} // --------------------------------------------------------------------------- // buildCodebaseMap @@ -25,13 +88,13 @@ const FIXTURE_ROOT = path.join(__dirname, 'fixtures', 'map-fixture'); describe('buildCodebaseMap', () => { it('returns a CodebaseMapSummary with project name from rootPath', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.project).toBe('map-fixture'); }); it('derives architecture layers from graph keys, sorted by count desc then alpha', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); // Use objectContaining — layers may now have hubFile/hubExports from enrichLayers expect(map.architecture.layers).toHaveLength(3); expect(map.architecture.layers[0]).toMatchObject({ name: 'src', fileCount: 5 }); @@ -47,7 +110,7 @@ describe('buildCodebaseMap', () => { it('derives hub files: top 5 by importedBy count, sorted count-desc then alpha', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.architecture.hubFiles).toEqual([ 'src/core/search.ts', 'src/utils/helpers.ts', @@ -57,7 +120,7 @@ describe('buildCodebaseMap', () => { it('derives active patterns from intelligence.json, sorted by adoption desc', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.activePatterns).toEqual([ { name: 'Injectable', adoption: '100%', trend: 'Stable' }, { name: 'RxJS', adoption: '72%', trend: 'Rising' }, @@ -67,7 +130,7 @@ describe('buildCodebaseMap', () => { it('derives best examples from goldenFiles with dominant pattern as reason', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.bestExamples).toEqual([ { file: 'src/core/search.ts', score: 0.95, reason: 'Injectable' }, { file: 'src/utils/helpers.ts', score: 0.87, reason: 'Injectable' } @@ -76,13 +139,13 @@ describe('buildCodebaseMap', () => { it('reads graph stats from relationships.json', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.graphStats).toEqual({ files: 8, edges: 9, avgDependencies: 1.1 }); }); it('adds suggested next calls: split pattern + golden file + fallback', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); // Vitest at 45% triggers split-pattern suggestion expect(map.suggestedNextCalls[0]).toEqual({ tool: 'get_team_patterns', @@ -107,7 +170,7 @@ describe('buildCodebaseMap', () => { it('degrades gracefully when intelligence.json is missing', async () => { // Point at a non-existent dir — builder should return empty map, not throw const project = createProjectState(path.join(FIXTURE_ROOT, 'nonexistent')); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.architecture.layers).toEqual([]); expect(map.architecture.entrypoints).toEqual([]); expect(map.architecture.hubFiles).toEqual([]); @@ -123,15 +186,246 @@ describe('buildCodebaseMap', () => { it('caps suggested next calls at 3', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); expect(map.suggestedNextCalls.length).toBeLessThanOrEqual(3); }); + it('keeps bounded mode free of tests, fixtures, generated output, dist, vendor, and bundled repos noise', async () => { + const rootPath = await createTempMapProject({ + projectName: 'codebase-context', + patterns: { + default: { + primary: { name: 'Factory', frequency: '80%', trend: 'Stable' } + } + }, + goldenFiles: [ + { file: 'tests/codebase-map.test.ts', score: 0.99 }, + { file: 'dist/index.js', score: 0.97 }, + { file: 'repos/external-lib/src/index.ts', score: 0.96 }, + { file: 'src/core/map.ts', score: 0.95 }, + { file: 'src/index.ts', score: 0.91 } + ], + graph: { + imports: { + 'src/index.ts': ['src/core/map.ts'], + 'src/cli.ts': ['src/core/map.ts'], + 'src/core/map.ts': [], + 'tests/codebase-map.test.ts': ['src/core/map.ts'], + 'dist/index.js': ['src/core/map.ts'], + 'repos/external-lib/src/index.ts': ['src/core/map.ts'], + 'vendor/acme/index.ts': ['src/core/map.ts'], + 'src/generated/api.generated.ts': ['src/core/map.ts'], + 'src/fixtures/sample.ts': ['src/core/map.ts'] + }, + importedBy: { + 'src/core/map.ts': [ + 'src/index.ts', + 'src/cli.ts', + 'tests/codebase-map.test.ts', + 'dist/index.js', + 'repos/external-lib/src/index.ts', + 'vendor/acme/index.ts', + 'src/generated/api.generated.ts', + 'src/fixtures/sample.ts' + ] + }, + exports: { + 'src/index.ts': [{ name: 'serve', type: 'function' }], + 'src/cli.ts': [{ name: 'runCli', type: 'function' }], + 'tests/codebase-map.test.ts': [{ name: 'suite', type: 'function' }], + 'dist/index.js': [{ name: 'bundle', type: 'function' }], + 'repos/external-lib/src/index.ts': [{ name: 'mountExternalRepo', type: 'function' }], + 'src/generated/api.generated.ts': [{ name: 'GeneratedApi', type: 'interface' }] + }, + stats: { files: 8, edges: 7, avgDependencies: 0.9 } + }, + chunks: [ + { + relativePath: 'src/core/map.ts', + content: 'export class MapBuilder { build() {} }', + metadata: { + symbolAware: true, + symbolKind: 'class', + symbolName: 'MapBuilder' + } + } as CodeChunk, + { + relativePath: 'tests/codebase-map.test.ts', + content: 'export class MapBuilderTest { run() {} }', + metadata: { + symbolAware: true, + symbolKind: 'class', + symbolName: 'MapBuilderTest' + } + } as CodeChunk, + { + relativePath: 'src/generated/api.generated.ts', + content: 'export interface GeneratedApi { id: string }', + metadata: { + symbolAware: true, + symbolKind: 'interface', + symbolName: 'GeneratedApi' + } + } as CodeChunk + ] + }); + + try { + const project = createProjectState(rootPath); + const map = await buildCodebaseMap(project); + + expect(map.project).toBe('codebase-context'); + expect(map.architecture.layers.map((layer) => layer.name)).toEqual(['src']); + expect(map.architecture.entrypoints).toEqual(['src/cli.ts', 'src/index.ts']); + expect(map.architecture.hubFiles).toEqual(['src/core/map.ts']); + expect(map.architecture.keyInterfaces.map((item) => item.name)).toEqual(['MapBuilder']); + expect(map.architecture.apiSurface.map((surface) => surface.file)).toEqual([ + 'src/cli.ts', + 'src/index.ts' + ]); + expect(map.architecture.hotspots.every((hotspot) => hotspot.file.startsWith('src/'))).toBe( + true + ); + expect(map.bestExamples).toEqual([ + { file: 'src/core/map.ts', score: 0.95, reason: 'Factory' }, + { file: 'src/index.ts', score: 0.91, reason: 'Factory' } + ]); + } finally { + await removeTempMapProject(rootPath); + } + }); + + it('restores excluded paths in full mode and removes bounded caps', async () => { + const imports: Record = {}; + const importedBy: Record = {}; + const exportsByFile: Record> = {}; + const chunks: CodeChunk[] = []; + const goldenFiles: Array<{ file: string; score: number }> = []; + + for (let index = 0; index < 10; index += 1) { + const contractFile = `src/contracts/contract-${index}.ts`; + importedBy[contractFile] = [`src/entry-${index}.ts`, 'tests/codebase-map.test.ts']; + chunks.push({ + relativePath: contractFile, + content: `export interface Contract${index} { value: string }`, + metadata: { + symbolAware: true, + symbolKind: 'interface', + symbolName: `Contract${index}` + } + } as CodeChunk); + goldenFiles.push({ file: contractFile, score: 0.9 - index * 0.01 }); + } + + for (let index = 0; index < 12; index += 1) { + const entryFile = `src/entry-${index}.ts`; + const sharedFile = `src/shared-${index}.ts`; + imports[entryFile] = [sharedFile]; + imports[sharedFile] = []; + importedBy[sharedFile] = [entryFile]; + exportsByFile[entryFile] = [ + { name: `entry${index}A`, type: 'function' }, + { name: `entry${index}B`, type: 'function' }, + { name: `entry${index}C`, type: 'function' }, + { name: `entry${index}D`, type: 'function' } + ]; + } + + imports['tests/codebase-map.test.ts'] = ['src/shared-0.ts']; + imports['dist/index.js'] = ['src/shared-1.ts']; + imports['repos/external-lib/src/index.ts'] = ['src/shared-2.ts']; + imports['vendor/acme/index.ts'] = ['src/shared-2.ts']; + exportsByFile['tests/codebase-map.test.ts'] = [{ name: 'suite', type: 'function' }]; + exportsByFile['dist/index.js'] = [{ name: 'bundle', type: 'function' }]; + exportsByFile['repos/external-lib/src/index.ts'] = [{ name: 'repoEntry', type: 'function' }]; + exportsByFile['vendor/acme/index.ts'] = [{ name: 'vendorEntry', type: 'function' }]; + goldenFiles.unshift( + { file: 'tests/codebase-map.test.ts', score: 0.99 }, + { file: 'dist/index.js', score: 0.98 }, + { file: 'repos/external-lib/src/index.ts', score: 0.975 }, + { file: 'vendor/acme/index.ts', score: 0.97 } + ); + + const rootPath = await createTempMapProject({ + graph: { + imports, + importedBy, + exports: exportsByFile, + stats: { files: 30, edges: 40, avgDependencies: 1.3 } + }, + goldenFiles, + chunks + }); + + try { + const project = createProjectState(rootPath); + const boundedMap = await buildCodebaseMap(project); + const fullMap = await buildCodebaseMap(project, { mode: 'full' }); + + expect(boundedMap.architecture.entrypoints).toHaveLength(BOUNDED_LIMITS.entrypoints); + expect(fullMap.architecture.entrypoints.length).toBeGreaterThan( + boundedMap.architecture.entrypoints.length + ); + expect(boundedMap.architecture.keyInterfaces).toHaveLength(BOUNDED_LIMITS.keyInterfaces); + expect(fullMap.architecture.keyInterfaces.length).toBeGreaterThan( + boundedMap.architecture.keyInterfaces.length + ); + expect(boundedMap.architecture.apiSurface).toHaveLength(BOUNDED_LIMITS.apiSurfaceFiles); + expect(fullMap.architecture.apiSurface.length).toBeGreaterThan( + boundedMap.architecture.apiSurface.length + ); + expect( + boundedMap.architecture.apiSurface.find((surface) => surface.file === 'src/entry-0.ts') + ?.exports + ).toHaveLength(BOUNDED_LIMITS.apiSurfaceExports); + expect( + fullMap.architecture.apiSurface.find((surface) => surface.file === 'src/entry-0.ts') + ?.exports + ).toHaveLength(4); + expect(boundedMap.architecture.hubFiles).toHaveLength(BOUNDED_LIMITS.hubFiles); + expect(fullMap.architecture.hubFiles.length).toBeGreaterThan( + boundedMap.architecture.hubFiles.length + ); + expect(boundedMap.architecture.hotspots).toHaveLength(BOUNDED_LIMITS.hotspots); + expect(fullMap.architecture.hotspots.length).toBeGreaterThan( + boundedMap.architecture.hotspots.length + ); + expect(boundedMap.bestExamples).toHaveLength(BOUNDED_LIMITS.bestExamples); + expect(fullMap.bestExamples.some((example) => example.file === 'tests/codebase-map.test.ts')).toBe( + true + ); + expect(fullMap.bestExamples.some((example) => example.file === 'dist/index.js')).toBe(true); + expect(fullMap.bestExamples.some((example) => example.file === 'repos/external-lib/src/index.ts')).toBe( + true + ); + expect(fullMap.architecture.layers.map((layer) => layer.name)).toEqual( + expect.arrayContaining(['dist', 'repos', 'tests', 'vendor']) + ); + } finally { + await removeTempMapProject(rootPath); + } + }); + + it('keeps the repo-root codebase-context map bounded by default', async () => { + const project = createProjectState(CURRENT_REPO_ROOT); + const map = await buildCodebaseMap(project); + + expect(map.project).toBe(path.basename(CURRENT_REPO_ROOT)); + expect(map.architecture.layers.map((layer) => layer.name)).not.toContain('tests'); + expect(map.architecture.layers.map((layer) => layer.name)).not.toContain('dist'); + expect(map.architecture.layers.map((layer) => layer.name)).not.toContain('repos'); + expect(map.architecture.entrypoints.length).toBeLessThanOrEqual(8); + expect(map.architecture.apiSurface.length).toBeLessThanOrEqual(8); + expect(map.architecture.hubFiles.every((file) => !/(?:^|\/)(?:tests?|dist)\//.test(file))).toBe( + true + ); + }); + // --- Structural skeleton (Phase 13) --- it('derives keyInterfaces from symbolAware chunks, sorted by importer count', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); // SearchOptions and CodebaseSearcher are both in src/core/search.ts (3 importers) // SearchResult is in src/types.ts (0 importers) // helperUtil is not symbolAware — excluded @@ -145,7 +439,7 @@ describe('buildCodebaseMap', () => { it('signatureHint strips trailing { and caps at 200 chars', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); for (const ki of map.architecture.keyInterfaces) { expect(ki.signatureHint).not.toMatch(/\{$/); expect(ki.signatureHint.length).toBeLessThanOrEqual(200); @@ -154,14 +448,14 @@ describe('buildCodebaseMap', () => { it('signatureHint contains the symbol name', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); const iface = map.architecture.keyInterfaces.find((k) => k.name === 'SearchOptions')!; expect(iface.signatureHint).toContain('SearchOptions'); }); it('derives apiSurface from entrypoints x graph.exports', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); // src/cli.ts and src/index.ts are entrypoints; both have exports in fixture const cli = map.architecture.apiSurface.find((s) => s.file === 'src/cli.ts'); expect(cli).toBeDefined(); @@ -172,7 +466,7 @@ describe('buildCodebaseMap', () => { it('apiSurface excludes default exports', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); for (const surface of map.architecture.apiSurface) { expect(surface.exports).not.toContain('default'); } @@ -182,9 +476,9 @@ describe('buildCodebaseMap', () => { const project = createProjectState(FIXTURE_ROOT); const map = await buildCodebaseMap(project); expect(map.architecture.hotspots.length).toBeLessThanOrEqual(5); - // src/core/search.ts: importedBy=3, imports=2 → combined=5 (highest) + // Bounded mode drops test importers, so search.ts keeps two real importers plus two imports. expect(map.architecture.hotspots[0].file).toBe('src/core/search.ts'); - expect(map.architecture.hotspots[0].combined).toBe(5); + expect(map.architecture.hotspots[0].combined).toBe(4); // combined is always importerCount + importCount for (const h of map.architecture.hotspots) { expect(h.combined).toBe(h.importerCount + h.importCount); @@ -193,7 +487,7 @@ describe('buildCodebaseMap', () => { it('enriches layers with hubFile from importedBy data', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); const srcLayer = map.architecture.layers.find((l) => l.name === 'src')!; // src/core/search.ts has 3 importers — highest in the src layer expect(srcLayer.hubFile).toBe('src/core/search.ts'); @@ -201,7 +495,7 @@ describe('buildCodebaseMap', () => { it('enriches layers with hubExports when graph.exports has data', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); // src/cli.ts has exports in fixture but is not the hub of the src layer // src/index.ts has exports and is also in src — but search.ts (hub) has no exports in fixture const srcLayer = map.architecture.layers.find((l) => l.name === 'src')!; @@ -248,7 +542,7 @@ describe('buildCodebaseMap', () => { ); const project = createProjectState(tempRoot); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); const srcLayer = map.architecture.layers.find((layer) => layer.name === 'src'); expect(srcLayer?.hubFile).toBe('src/a.ts'); @@ -273,7 +567,7 @@ describe('renderMapMarkdown', () => { it('includes all required section headers', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); const md = renderMapMarkdown(map); expect(md).toContain('# Codebase Map'); expect(md).toContain('## Architecture Layers'); @@ -320,7 +614,7 @@ describe('renderMapMarkdown', () => { describe('renderMapPretty', () => { it('renders box characters in default mode', async () => { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); const pretty = renderMapPretty(map); expect(pretty).toContain('┌'); expect(pretty).toContain('│'); @@ -332,7 +626,7 @@ describe('renderMapPretty', () => { process.env.CODEBASE_CONTEXT_ASCII = '1'; try { const project = createProjectState(FIXTURE_ROOT); - const map = await buildCodebaseMap(project); + const map = await buildCodebaseMap(project, { mode: 'full' }); const pretty = renderMapPretty(map); expect(pretty).toContain('+'); expect(pretty).toContain('-'); diff --git a/tests/multi-project-routing.test.ts b/tests/multi-project-routing.test.ts index 6240082..fa8f181 100644 --- a/tests/multi-project-routing.test.ts +++ b/tests/multi-project-routing.test.ts @@ -11,7 +11,12 @@ import { KEYWORD_INDEX_FILENAME, VECTOR_DB_DIRNAME } from '../src/constants/codebase-context.js'; -import { CONTEXT_RESOURCE_URI, buildProjectContextResourceUri } from '../src/resources/uri.js'; +import { + CONTEXT_RESOURCE_URI, + FULL_CONTEXT_RESOURCE_URI, + buildProjectContextResourceUri, + buildProjectFullContextResourceUri +} from '../src/resources/uri.js'; interface SearchResultRow { summary: string; @@ -583,6 +588,64 @@ describe('multi-project routing', () => { expect(response.contents[0]?.text).not.toContain('Project selection required'); }); + it('lists bounded and full context resources for active and project-scoped flows', async () => { + const { server } = await import('../src/index.js'); + const toolHandler = (server as unknown as TestServer)._requestHandlers.get('tools/call'); + const resourcesHandler = (server as unknown as TestServer)._requestHandlers.get('resources/list'); + + if (!toolHandler || !resourcesHandler) { + throw new Error('required handlers not registered'); + } + + await callTool(toolHandler, 140, 'search_codebase', { + query: 'feature', + project: secondaryRoot + }); + + const response = (await resourcesHandler({ + jsonrpc: '2.0', + id: 141, + method: 'resources/list', + params: {} + })) as { resources: Array<{ uri: string }> }; + + const uris = response.resources.map((resource) => resource.uri); + expect(uris).toContain(CONTEXT_RESOURCE_URI); + expect(uris).toContain(FULL_CONTEXT_RESOURCE_URI); + expect(uris).toContain(buildProjectContextResourceUri(primaryRoot)); + expect(uris).toContain(buildProjectFullContextResourceUri(primaryRoot)); + expect(uris).toContain(buildProjectContextResourceUri(secondaryRoot)); + expect(uris).toContain(buildProjectFullContextResourceUri(secondaryRoot)); + }); + + it('generic full context resource follows the active project after selection', async () => { + const { server } = await import('../src/index.js'); + const requestHandler = (server as unknown as TestServer)._requestHandlers.get('tools/call'); + const resourceHandler = (server as unknown as TestServer)._requestHandlers.get( + 'resources/read' + ); + + if (!requestHandler || !resourceHandler) { + throw new Error('required handlers not registered'); + } + + await callTool(requestHandler, 142, 'search_codebase', { + query: 'feature', + project: secondaryRoot + }); + + const response = (await resourceHandler({ + jsonrpc: '2.0', + id: 143, + method: 'resources/read', + params: { uri: FULL_CONTEXT_RESOURCE_URI } + })) as ResourceReadResponse; + + expect(response.contents[0]?.uri).toBe(FULL_CONTEXT_RESOURCE_URI); + expect(response.contents[0]?.text).toContain('# Codebase Map'); + expect(response.contents[0]?.text).not.toContain('Project selection required'); + }); + it('builds a workspace overview for multiple configured roots before selection', async () => { const { server, refreshKnownRootsFromClient } = await import('../src/index.js'); const typedServer = server as unknown as TestServer & { @@ -615,6 +678,7 @@ describe('multi-project routing', () => { 'client-announced roots as the workspace boundary' ); expect(response.contents[0]?.text).toContain('codebase://context/project/'); + expect(response.contents[0]?.text).toContain('codebase://context/full/project/'); expect(response.contents[0]?.text).toContain('retry tool calls with `project`'); expect(response.contents[0]?.text).toContain('apps/dashboard'); expect(response.contents[0]?.text).toMatch(/\[(idle|indexing|ready)\]/); @@ -661,6 +725,18 @@ describe('multi-project routing', () => { expect(response.contents[0]?.uri).toBe(buildProjectContextResourceUri(payload.project.project)); expect(response.contents[0]?.text).toContain('# Codebase Map'); + + const fullResponse = (await resourceHandler({ + jsonrpc: '2.0', + id: 171, + method: 'resources/read', + params: { uri: buildProjectFullContextResourceUri(payload.project.project) } + })) as ResourceReadResponse; + + expect(fullResponse.contents[0]?.uri).toBe( + buildProjectFullContextResourceUri(payload.project.project) + ); + expect(fullResponse.contents[0]?.text).toContain('# Codebase Map'); }); it('returns unknown_project error when project path does not exist', async () => { diff --git a/tests/resource-uri.test.ts b/tests/resource-uri.test.ts index 37757f6..11d3c15 100644 --- a/tests/resource-uri.test.ts +++ b/tests/resource-uri.test.ts @@ -1,8 +1,12 @@ import { describe, it, expect } from 'vitest'; import { buildProjectContextResourceUri, + buildProjectFullContextResourceUri, CONTEXT_RESOURCE_URI, + FULL_CONTEXT_RESOURCE_URI, + getProjectPathFromFullContextResourceUri, getProjectPathFromContextResourceUri, + isFullContextResourceUri, isContextResourceUri, normalizeResourceUri } from '../src/resources/uri.js'; @@ -13,12 +17,23 @@ describe('resource URI normalization', () => { expect(isContextResourceUri(CONTEXT_RESOURCE_URI)).toBe(true); }); + it('accepts canonical full resource URI', () => { + expect(normalizeResourceUri(FULL_CONTEXT_RESOURCE_URI)).toBe(FULL_CONTEXT_RESOURCE_URI); + expect(isFullContextResourceUri(FULL_CONTEXT_RESOURCE_URI)).toBe(true); + }); + it('accepts namespaced resource URI from some MCP hosts', () => { const namespaced = `codebase-context/${CONTEXT_RESOURCE_URI}`; expect(normalizeResourceUri(namespaced)).toBe(CONTEXT_RESOURCE_URI); expect(isContextResourceUri(namespaced)).toBe(true); }); + it('accepts namespaced full resource URI from some MCP hosts', () => { + const namespaced = `codebase-context/${FULL_CONTEXT_RESOURCE_URI}`; + expect(normalizeResourceUri(namespaced)).toBe(FULL_CONTEXT_RESOURCE_URI); + expect(isFullContextResourceUri(namespaced)).toBe(true); + }); + it('round-trips project-scoped context URIs', () => { const projectPath = '/repo/apps/dashboard'; const uri = buildProjectContextResourceUri(projectPath); @@ -27,9 +42,19 @@ describe('resource URI normalization', () => { expect(getProjectPathFromContextResourceUri(`host/${uri}`)).toBe(projectPath); }); + it('round-trips project-scoped full context URIs', () => { + const projectPath = '/repo/apps/dashboard'; + const uri = buildProjectFullContextResourceUri(projectPath); + expect(uri).toBe('codebase://context/full/project/%2Frepo%2Fapps%2Fdashboard'); + expect(getProjectPathFromFullContextResourceUri(uri)).toBe(projectPath); + expect(getProjectPathFromFullContextResourceUri(`host/${uri}`)).toBe(projectPath); + }); + it('rejects unknown URIs', () => { expect(isContextResourceUri('codebase://other')).toBe(false); + expect(isFullContextResourceUri('codebase://other')).toBe(false); expect(isContextResourceUri('other/codebase://other')).toBe(false); expect(getProjectPathFromContextResourceUri('codebase://other')).toBeUndefined(); + expect(getProjectPathFromFullContextResourceUri('codebase://other')).toBeUndefined(); }); });