Skip to content

Commit fda679b

Browse files
committed
feat(map): bound default output and full context
1 parent 0458be8 commit fda679b

9 files changed

Lines changed: 672 additions & 73 deletions

File tree

src/cli-map.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ function printMapUsage(): void {
3232
console.log('Output the conventions map for the current codebase.');
3333
console.log('');
3434
console.log('Options:');
35-
console.log(' --export Write CODEBASE_MAP.md to project root (overrides other flags)');
35+
console.log(' --export Write CODEBASE_MAP.md to project root (still honors --full)');
36+
console.log(' --full Output the exhaustive map instead of the bounded default');
3637
console.log(' --json Output raw JSON (CodebaseMapSummary)');
3738
console.log(' --pretty Terminal-friendly box layout');
3839
console.log(' --help Show this help');
@@ -44,6 +45,7 @@ export async function handleMapCli(args: string[]): Promise<void> {
4445
const useJson = args.includes('--json');
4546
const usePretty = args.includes('--pretty');
4647
const useExport = args.includes('--export');
48+
const useFull = args.includes('--full');
4749
const showHelp = args.includes('--help') || args.includes('-h');
4850

4951
if (showHelp) {
@@ -77,7 +79,7 @@ export async function handleMapCli(args: string[]): Promise<void> {
7779
project.indexState = indexState;
7880

7981
try {
80-
const map = await buildCodebaseMap(project);
82+
const map = await buildCodebaseMap(project, { mode: useFull ? 'full' : 'bounded' });
8183

8284
if (useExport) {
8385
const outPath = path.join(rootPath, 'CODEBASE_MAP.md');

src/core/codebase-map.ts

Lines changed: 140 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import type {
2424
PatternsData,
2525
CodeChunk
2626
} from '../types/index.js';
27-
import { RELATIONSHIPS_FILENAME, KEYWORD_INDEX_FILENAME } from '../constants/codebase-context.js';
27+
import {
28+
EXCLUDED_DIRECTORY_NAMES,
29+
RELATIONSHIPS_FILENAME,
30+
KEYWORD_INDEX_FILENAME
31+
} from '../constants/codebase-context.js';
2832

2933
// ---------------------------------------------------------------------------
3034
// Internal types for relationships.json
@@ -50,12 +54,37 @@ interface RelationshipsData {
5054
};
5155
}
5256

53-
// ---------------------------------------------------------------------------
54-
// Entrypoint exclusion pattern
55-
// ---------------------------------------------------------------------------
56-
57-
const ENTRYPOINT_EXCLUSION_RE =
58-
/(?:^|\/)(?:tests?|__tests__|fixtures?|scripts?)\/|\.test\.|\.spec\./;
57+
type CodebaseMapMode = 'bounded' | 'full';
58+
59+
type BuildCodebaseMapOptions = {
60+
mode?: CodebaseMapMode;
61+
};
62+
63+
const BOUNDED_SECTION_LIMITS = {
64+
entrypoints: 8,
65+
hubFiles: 5,
66+
keyInterfaces: 8,
67+
apiSurfaceFiles: 8,
68+
apiSurfaceExports: 3,
69+
hotspots: 5,
70+
bestExamples: 3
71+
} as const;
72+
73+
const MAP_EXCLUDED_PATH_PATTERNS = [
74+
/^repos\/[^/]+(?:\/|$)/i,
75+
/(?:^|\/)(?:tests?|__tests__|specs?|__specs__)(?:\/|$)/i,
76+
/\.(?:test|spec)\.[^/]+$/i,
77+
/(?:^|\/)(?:fixtures?|__fixtures__)(?:\/|$)/i,
78+
/(?:^|\/)(?:generated|__generated__)(?:\/|$)/i,
79+
/(?:^|\/)[^/]*\.(?:generated|gen|min)\.[^/]+$/i,
80+
/\.snap$/i
81+
] as const;
82+
83+
const MAP_EXCLUDED_DIRECTORY_NAMES = new Set(
84+
[...EXCLUDED_DIRECTORY_NAMES, '__fixtures__', '__generated__', 'fixtures', 'generated'].map(
85+
(segment) => segment.toLowerCase()
86+
)
87+
);
5988

6089
// ---------------------------------------------------------------------------
6190
// Builder
@@ -66,7 +95,11 @@ const ENTRYPOINT_EXCLUSION_RE =
6695
* Reads `intelligence.json`, `relationships.json`, and `index.json` from project paths.
6796
* Degrades gracefully when artifacts are missing.
6897
*/
69-
export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseMapSummary> {
98+
export async function buildCodebaseMap(
99+
project: ProjectState,
100+
options: BuildCodebaseMapOptions = {}
101+
): Promise<CodebaseMapSummary> {
102+
const mode = options.mode ?? 'bounded';
70103
const projectName = path.basename(project.rootPath);
71104

72105
// Read intelligence.json
@@ -100,9 +133,10 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
100133
}
101134

102135
const graph = relationships.graph ?? {};
103-
const graphImports = graph.imports ?? {};
104-
const graphImportedBy = graph.importedBy ?? {};
105-
const graphExports = graph.exports ?? {};
136+
const graphImports = filterAdjacencyGraph(graph.imports ?? {}, mode);
137+
const graphImportedBy = filterAdjacencyGraph(graph.importedBy ?? {}, mode);
138+
const graphExports = filterExportGraph(graph.exports ?? {}, mode);
139+
const filteredChunks = chunks.filter((chunk) => isMapEligiblePath(chunk.relativePath, mode));
106140
// relationships.json has stats at top level OR inside graph
107141
const statsSource =
108142
relationships.stats ??
@@ -133,13 +167,13 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
133167
const entrypoints: string[] = [];
134168
for (const [file, imports] of Object.entries(graphImports)) {
135169
if (imports.length === 0) continue; // no imports — not an entrypoint
136-
if (ENTRYPOINT_EXCLUSION_RE.test(file)) continue; // test/script file
137170
const importers = graphImportedBy[file];
138171
if (!importers || importers.length === 0) {
139172
entrypoints.push(file);
140173
}
141174
}
142175
entrypoints.sort();
176+
const boundedEntrypoints = maybeLimit(entrypoints, BOUNDED_SECTION_LIMITS.entrypoints, mode);
143177

144178
// --- Hub files ---
145179
const importedByCounts: Array<{ file: string; count: number }> = Object.entries(
@@ -150,17 +184,17 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
150184
(x) => x.count,
151185
(x) => x.file
152186
)
153-
.slice(0, 5)
187+
.slice(0, mode === 'bounded' ? BOUNDED_SECTION_LIMITS.hubFiles : undefined)
154188
.map((x) => x.file);
155189

156190
// --- Key interfaces ---
157-
const keyInterfaces = deriveKeyInterfaces(chunks, graphImportedBy);
191+
const keyInterfaces = deriveKeyInterfaces(filteredChunks, graphImportedBy, mode);
158192

159193
// --- API surface ---
160-
const apiSurface = deriveApiSurface(entrypoints, graphExports);
194+
const apiSurface = deriveApiSurface(boundedEntrypoints, graphExports, mode);
161195

162196
// --- Dependency hotspots ---
163-
const hotspots = deriveHotspots(graphImports, graphImportedBy);
197+
const hotspots = deriveHotspots(graphImports, graphImportedBy, mode);
164198

165199
// --- Active patterns ---
166200
const patterns: PatternsData = intelligence.patterns ?? {};
@@ -187,8 +221,12 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
187221
// --- Best examples ---
188222
const dominantPatternName =
189223
activePatterns.length > 0 ? activePatterns[0].name : 'high-quality example';
190-
const goldenFiles = intelligence.goldenFiles ?? [];
191-
const bestExamples: CodebaseMapExample[] = goldenFiles.slice(0, 3).map((gf) => ({
224+
const goldenFiles = (intelligence.goldenFiles ?? []).filter((gf) => isMapEligiblePath(gf.file, mode));
225+
const bestExamples: CodebaseMapExample[] = maybeLimit(
226+
goldenFiles,
227+
BOUNDED_SECTION_LIMITS.bestExamples,
228+
mode
229+
).map((gf) => ({
192230
file: gf.file,
193231
score: gf.score,
194232
reason: dominantPatternName
@@ -210,7 +248,14 @@ export async function buildCodebaseMap(project: ProjectState): Promise<CodebaseM
210248

211249
return {
212250
project: projectName,
213-
architecture: { layers, entrypoints, hubFiles, keyInterfaces, apiSurface, hotspots },
251+
architecture: {
252+
layers,
253+
entrypoints: boundedEntrypoints,
254+
hubFiles,
255+
keyInterfaces,
256+
apiSurface,
257+
hotspots
258+
},
214259
activePatterns,
215260
bestExamples,
216261
graphStats,
@@ -236,7 +281,8 @@ function buildSignatureHint(content: string): string {
236281

237282
function deriveKeyInterfaces(
238283
chunks: CodeChunk[],
239-
graphImportedBy: Record<string, string[]>
284+
graphImportedBy: Record<string, string[]>,
285+
mode: CodebaseMapMode
240286
): CodebaseMapKeyInterface[] {
241287
const symbolChunks = chunks.filter(
242288
(c) => c.metadata?.symbolAware === true && SYMBOL_KINDS.has(c.metadata.symbolKind ?? '')
@@ -251,18 +297,21 @@ function deriveKeyInterfaces(
251297
if (lenDiff !== 0) return lenDiff;
252298
return a.chunk.relativePath.localeCompare(b.chunk.relativePath);
253299
});
254-
return scored.slice(0, 10).map(({ chunk, importerCount }) => ({
255-
name: chunk.metadata.symbolName ?? path.basename(chunk.relativePath),
256-
kind: chunk.metadata.symbolKind ?? 'unknown',
257-
file: chunk.relativePath,
258-
importerCount,
259-
signatureHint: buildSignatureHint(chunk.content)
260-
}));
300+
return maybeLimit(scored, BOUNDED_SECTION_LIMITS.keyInterfaces, mode).map(
301+
({ chunk, importerCount }) => ({
302+
name: chunk.metadata.symbolName ?? path.basename(chunk.relativePath),
303+
kind: chunk.metadata.symbolKind ?? 'unknown',
304+
file: chunk.relativePath,
305+
importerCount,
306+
signatureHint: buildSignatureHint(chunk.content)
307+
})
308+
);
261309
}
262310

263311
function deriveApiSurface(
264312
entrypoints: string[],
265-
graphExports: Record<string, Array<{ name: string; type: string }>>
313+
graphExports: Record<string, Array<{ name: string; type: string }>>,
314+
mode: CodebaseMapMode
266315
): CodebaseMapApiSurface[] {
267316
const results: CodebaseMapApiSurface[] = [];
268317
for (const ep of entrypoints) {
@@ -271,16 +320,17 @@ function deriveApiSurface(
271320
const names = exps
272321
.map((e) => e.name)
273322
.filter((n) => n && n !== 'default')
274-
.slice(0, 5);
323+
.slice(0, mode === 'bounded' ? BOUNDED_SECTION_LIMITS.apiSurfaceExports : undefined);
275324
if (names.length === 0) continue;
276325
results.push({ file: ep, exports: names });
277326
}
278-
return results;
327+
return maybeLimit(results, BOUNDED_SECTION_LIMITS.apiSurfaceFiles, mode);
279328
}
280329

281330
function deriveHotspots(
282331
graphImports: Record<string, string[]>,
283-
graphImportedBy: Record<string, string[]>
332+
graphImportedBy: Record<string, string[]>,
333+
mode: CodebaseMapMode
284334
): CodebaseMapHotspot[] {
285335
const allFiles = new Set([...Object.keys(graphImports), ...Object.keys(graphImportedBy)]);
286336
const hotspots: CodebaseMapHotspot[] = [];
@@ -295,7 +345,7 @@ function deriveHotspots(
295345
if (b.combined !== a.combined) return b.combined - a.combined;
296346
return a.file.localeCompare(b.file);
297347
});
298-
return hotspots.slice(0, 5);
348+
return maybeLimit(hotspots, BOUNDED_SECTION_LIMITS.hotspots, mode);
299349
}
300350

301351
function enrichLayers(
@@ -642,3 +692,61 @@ function sortByCountThenAlpha<T>(
642692
return getName(a).localeCompare(getName(b));
643693
});
644694
}
695+
696+
function maybeLimit<T>(items: T[], limit: number, mode: CodebaseMapMode): T[] {
697+
return mode === 'bounded' ? items.slice(0, limit) : items;
698+
}
699+
700+
function filterAdjacencyGraph(
701+
graph: Record<string, string[]>,
702+
mode: CodebaseMapMode
703+
): Record<string, string[]> {
704+
if (mode === 'full') {
705+
return graph;
706+
}
707+
708+
return Object.fromEntries(
709+
Object.entries(graph)
710+
.filter(([file]) => isMapEligiblePath(file, mode))
711+
.map(([file, related]) => [file, related.filter((item) => isMapEligiblePath(item, mode))])
712+
);
713+
}
714+
715+
function filterExportGraph(
716+
graph: Record<string, Array<{ name: string; type: string }>>,
717+
mode: CodebaseMapMode
718+
): Record<string, Array<{ name: string; type: string }>> {
719+
if (mode === 'full') {
720+
return graph;
721+
}
722+
723+
return Object.fromEntries(
724+
Object.entries(graph).filter(([file]) => isMapEligiblePath(file, mode))
725+
);
726+
}
727+
728+
function isMapEligiblePath(filePath: string, mode: CodebaseMapMode): boolean {
729+
if (mode === 'full') {
730+
return true;
731+
}
732+
733+
const normalizedPath = normalizeMapPath(filePath);
734+
if (!normalizedPath) {
735+
return false;
736+
}
737+
738+
const segments = normalizedPath
739+
.split('/')
740+
.map((segment) => segment.toLowerCase())
741+
.filter(Boolean);
742+
743+
if (segments.some((segment) => MAP_EXCLUDED_DIRECTORY_NAMES.has(segment))) {
744+
return false;
745+
}
746+
747+
return !MAP_EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath));
748+
}
749+
750+
function normalizeMapPath(filePath: string): string {
751+
return filePath.replace(/\\/g, '/').replace(/^\.\//, '').trim();
752+
}

0 commit comments

Comments
 (0)