Skip to content

Commit f6772da

Browse files
colbymchenryclaude
andauthored
feat: zero-config indexing driven by .gitignore (colbymchenry#283) (colbymchenry#285)
Remove .codegraph/config.json and the entire config surface. CodeGraph now indexes every file whose extension maps to a supported language and respects .gitignore everywhere — git repos via git itself, non-git projects via the `ignore` library (root + nested .gitignore files, the same way git does). - Remove CodeGraphConfig/DEFAULT_CONFIG, src/config.ts, and the public config API (the `config` option on init, getConfig/updateConfig/getConfigPath). - Derive the source-file allowlist from EXTENSION_MAP (isSourceFile); maxFileSize is now a constant. Drop the .codegraphignore marker. - Behavior change: committed, non-gitignored dirs (vendor/, a committed dist/) are now indexed — .gitignore is the single source of truth. Earlier inert fields (languages, frameworks, extractDocstrings, trackCallSites, customPatterns) and their dead helpers are removed as part of this. Resolves colbymchenry#283. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5b71a89 commit f6772da

16 files changed

Lines changed: 225 additions & 998 deletions

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2929
grammar, and `web/core`, `web/modules/contrib`, `web/themes/contrib` are
3030
excluded by default. Resolves [#268](https://github.com/colbymchenry/codegraph/issues/268).
3131

32+
### Changed
33+
- **Zero-config indexing that respects `.gitignore`.** CodeGraph no longer has a
34+
config file. It indexes every file whose extension maps to a supported language
35+
and honors your `.gitignore` everywhere: in git repos via git itself, and in
36+
non-git projects (e.g. a freshly-scaffolded app before `git init`) by reading
37+
`.gitignore` files directly — root and nested, the same way git does (via the
38+
`ignore` library, so negation/anchoring/nested rules all behave correctly). To
39+
keep something out of the graph, add it to `.gitignore`. **Behavior change:**
40+
committed files that are *not* gitignored are now indexed even under `vendor/`,
41+
`Pods/`, or a committed `dist/` — previously a hardcoded exclude list skipped
42+
those names; now `.gitignore` is the single source of truth. Resolves
43+
[#283](https://github.com/colbymchenry/codegraph/issues/283).
44+
45+
### Removed
46+
- **`.codegraph/config.json` and the entire config surface.** Every field was
47+
either inert or now redundant with `.gitignore`:
48+
- `languages`/`frameworks` never affected indexing (languages are detected per
49+
file from extensions; frameworks are auto-detected). `languages` was also
50+
broken — its validator only knew the original 8 languages, so setting it to
51+
anything newer (C#, PHP, Ruby, C/C++, Swift, Kotlin, Dart, Vue, Scala, Lua, …)
52+
threw `Invalid configuration format`.
53+
- `extractDocstrings`/`trackCallSites`/`customPatterns` were never read by any
54+
extractor.
55+
- `include` is now derived from the supported language extensions, `exclude` is
56+
replaced by `.gitignore`, and `maxFileSize` (1 MB) is a constant.
57+
58+
**Breaking (library API):** the `CodeGraphConfig` type, the `config` option on
59+
`CodeGraph.init()`, and the `getConfig()`/`updateConfig()`/`getConfigPath`
60+
exports are gone. Existing `.codegraph/config.json` files are simply ignored.
61+
The `.codegraphignore` marker is no longer supported — use `.gitignore`.
62+
3263
## [0.9.1] - 2026-05-21
3364

3465
### Fixed

README.md

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -418,28 +418,23 @@ cg.close();
418418

419419
## Configuration
420420

421-
The `.codegraph/config.json` file controls indexing:
422-
423-
```json
424-
{
425-
"version": 1,
426-
"languages": ["typescript", "javascript"],
427-
"exclude": ["node_modules/**", "dist/**", "build/**", "*.min.js"],
428-
"frameworks": [],
429-
"maxFileSize": 1048576,
430-
"extractDocstrings": true,
431-
"trackCallSites": true
432-
}
433-
```
434-
435-
| Option | Description | Default |
436-
|--------|-------------|---------|
437-
| `languages` | Languages to index (auto-detected if empty) | `[]` |
438-
| `exclude` | Glob patterns to ignore | `["node_modules/**", ...]` |
439-
| `frameworks` | Framework hints for better resolution | `[]` |
440-
| `maxFileSize` | Skip files larger than this (bytes) | `1048576` (1MB) |
441-
| `extractDocstrings` | Extract docstrings from code | `true` |
442-
| `trackCallSites` | Track call site locations | `true` |
421+
There isn't any — CodeGraph is zero-config. It indexes every file whose
422+
extension maps to a [supported language](#supported-languages) and **respects
423+
your `.gitignore`**: in git repos via git itself, and in non-git projects by
424+
reading `.gitignore` files directly (root and nested, the same way git would).
425+
426+
What that means in practice:
427+
428+
- Anything git ignores — `node_modules`, build output, secrets in `.env` — is
429+
never indexed. **To keep something out of the graph, add it to `.gitignore`.**
430+
- There's no config file to write or keep in sync, and nothing to wire up per
431+
language: support is automatic from the file extension.
432+
- Files larger than 1 MB are skipped (generated bundles, minified JS, vendored
433+
blobs) — they cost parse budget for no useful symbols.
434+
435+
> Committed files that aren't gitignored *are* indexed, even under `vendor/` or a
436+
> committed `dist/`. If you commit a dependency or build directory you don't want
437+
> in the graph, add it to `.gitignore`.
443438
444439
## Supported Languages
445440

__tests__/extraction.test.ts

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ import * as fs from 'fs';
99
import * as path from 'path';
1010
import * as os from 'os';
1111
import { CodeGraph } from '../src';
12-
import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction';
12+
import { extractFromSource, scanDirectory } from '../src/extraction';
1313
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars } from '../src/extraction/grammars';
1414
import { normalizePath } from '../src/utils';
15-
import { DEFAULT_CONFIG } from '../src/types';
1615

1716
beforeAll(async () => {
1817
await initGrammars();
@@ -3003,48 +3002,65 @@ describe('Directory Exclusion', () => {
30033002
cleanupTempDir(tempDir);
30043003
});
30053004

3006-
it('should exclude node_modules directories', () => {
3007-
// Create structure: src/index.ts + node_modules/pkg/index.js
3005+
it('should exclude directories listed in .gitignore', () => {
3006+
// Create structure: src/index.ts + node_modules/pkg/index.js, gitignore node_modules
30083007
const srcDir = path.join(tempDir, 'src');
30093008
const nmDir = path.join(tempDir, 'node_modules', 'pkg');
30103009
fs.mkdirSync(srcDir, { recursive: true });
30113010
fs.mkdirSync(nmDir, { recursive: true });
30123011
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
30133012
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
3013+
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');
30143014

3015-
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
3016-
const files = scanDirectory(tempDir, config);
3015+
const files = scanDirectory(tempDir);
30173016

30183017
expect(files).toContain('src/index.ts');
30193018
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
30203019
});
30213020

3022-
it('should exclude nested node_modules directories', () => {
3023-
// Create structure: packages/app/node_modules/pkg/index.js
3021+
it('should exclude nested node_modules via a root .gitignore', () => {
3022+
// A trailing-slash pattern with no leading slash matches at any depth.
30243023
const srcDir = path.join(tempDir, 'packages', 'app', 'src');
30253024
const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg');
30263025
fs.mkdirSync(srcDir, { recursive: true });
30273026
fs.mkdirSync(nmDir, { recursive: true });
30283027
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
30293028
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
3029+
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');
30303030

3031-
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
3032-
const files = scanDirectory(tempDir, config);
3031+
const files = scanDirectory(tempDir);
30333032

30343033
expect(files).toContain('packages/app/src/index.ts');
30353034
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
30363035
});
30373036

3038-
it('should exclude .git directories', () => {
3037+
it('should apply a nested .gitignore only to its own subtree', () => {
3038+
const appSrc = path.join(tempDir, 'app', 'src');
3039+
fs.mkdirSync(appSrc, { recursive: true });
3040+
fs.writeFileSync(path.join(appSrc, 'keep.ts'), 'export const a = 1;');
3041+
fs.writeFileSync(path.join(appSrc, 'skip.ts'), 'export const b = 2;');
3042+
fs.writeFileSync(path.join(tempDir, 'app', '.gitignore'), 'src/skip.ts\n');
3043+
// A sibling with the same name outside app/ must NOT be ignored.
3044+
const otherDir = path.join(tempDir, 'other', 'src');
3045+
fs.mkdirSync(otherDir, { recursive: true });
3046+
fs.writeFileSync(path.join(otherDir, 'skip.ts'), 'export const c = 3;');
3047+
3048+
const files = scanDirectory(tempDir);
3049+
3050+
expect(files).toContain('app/src/keep.ts');
3051+
expect(files).not.toContain('app/src/skip.ts');
3052+
expect(files).toContain('other/src/skip.ts');
3053+
});
3054+
3055+
it('should always skip .git directories', () => {
30393056
const srcDir = path.join(tempDir, 'src');
30403057
const gitDir = path.join(tempDir, '.git', 'objects');
30413058
fs.mkdirSync(srcDir, { recursive: true });
30423059
fs.mkdirSync(gitDir, { recursive: true });
30433060
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
30443061
fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;');
30453062

3046-
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
3047-
const files = scanDirectory(tempDir, config);
3063+
const files = scanDirectory(tempDir);
30483064

30493065
expect(files).toContain('src/index.ts');
30503066
expect(files.every((f) => !f.includes('.git'))).toBe(true);
@@ -3055,29 +3071,12 @@ describe('Directory Exclusion', () => {
30553071
fs.mkdirSync(srcDir, { recursive: true });
30563072
fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}');
30573073

3058-
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
3059-
const files = scanDirectory(tempDir, config);
3074+
const files = scanDirectory(tempDir);
30603075

30613076
expect(files.length).toBe(1);
30623077
expect(files[0]).toBe('src/components/Button.tsx');
30633078
expect(files[0]).not.toContain('\\');
30643079
});
3065-
3066-
it('should respect .codegraphignore marker', () => {
3067-
const srcDir = path.join(tempDir, 'src');
3068-
const vendorDir = path.join(tempDir, 'vendor');
3069-
fs.mkdirSync(srcDir, { recursive: true });
3070-
fs.mkdirSync(vendorDir, { recursive: true });
3071-
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
3072-
fs.writeFileSync(path.join(vendorDir, 'lib.ts'), 'export const y = 2;');
3073-
fs.writeFileSync(path.join(vendorDir, '.codegraphignore'), '');
3074-
3075-
const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
3076-
const files = scanDirectory(tempDir, config);
3077-
3078-
expect(files).toContain('src/index.ts');
3079-
expect(files.every((f) => !f.includes('vendor'))).toBe(true);
3080-
});
30813080
});
30823081

30833082
describe('Git Submodules', () => {
@@ -3124,8 +3123,7 @@ describe('Git Submodules', () => {
31243123
);
31253124
git(mainDir, 'commit', '-q', '-m', 'add submodule');
31263125

3127-
const config = { ...DEFAULT_CONFIG, rootDir: mainDir };
3128-
const files = scanDirectory(mainDir, config);
3126+
const files = scanDirectory(mainDir);
31293127

31303128
expect(files).toContain('app.ts');
31313129
expect(files).toContain('libs/lib/lib.ts');
@@ -3173,8 +3171,7 @@ describe('Nested non-submodule git repos', () => {
31733171
git(path.join(root, 'sub_repo2'), 'init', '-q');
31743172
fs.writeFileSync(path.join(sub2, 'two.ts'), 'export const two = 2;');
31753173

3176-
const config = { ...DEFAULT_CONFIG, rootDir: root };
3177-
const files = scanDirectory(root, config);
3174+
const files = scanDirectory(root);
31783175

31793176
// Both committed and untracked source from the nested repos must be found.
31803177
expect(files).toContain('sub_repo1/src/one.ts');
@@ -3197,8 +3194,7 @@ describe('Nested non-submodule git repos', () => {
31973194
fs.writeFileSync(path.join(sub, 'real.ts'), 'export const real = 1;');
31983195
fs.writeFileSync(path.join(sub, 'generated.ts'), 'export const generated = 1;');
31993196

3200-
const config = { ...DEFAULT_CONFIG, rootDir: root };
3201-
const files = scanDirectory(root, config);
3197+
const files = scanDirectory(root);
32023198

32033199
expect(files).toContain('sub_repo/src/real.ts');
32043200
expect(files).not.toContain('sub_repo/src/generated.ts');

__tests__/foundation.test.ts

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import * as fs from 'fs';
99
import * as path from 'path';
1010
import * as os from 'os';
1111
import { CodeGraph } from '../src';
12-
import { DEFAULT_CONFIG, Node, Edge } from '../src/types';
13-
import { loadConfig, saveConfig } from '../src/config';
12+
import { Node, Edge } from '../src/types';
1413
import { isInitialized, getCodeGraphDir, validateDirectory } from '../src/directory';
1514
import { DatabaseConnection, getDatabasePath } from '../src/db';
1615

@@ -60,41 +59,12 @@ describe('CodeGraph Foundation', () => {
6059
cg.close();
6160
});
6261

63-
it('should create config.json with defaults', () => {
64-
const cg = CodeGraph.initSync(tempDir);
65-
66-
const configPath = path.join(getCodeGraphDir(tempDir), 'config.json');
67-
expect(fs.existsSync(configPath)).toBe(true);
68-
69-
const config = cg.getConfig();
70-
expect(config.version).toBe(DEFAULT_CONFIG.version);
71-
expect(config.include).toEqual(DEFAULT_CONFIG.include);
72-
expect(config.exclude).toEqual(DEFAULT_CONFIG.exclude);
73-
74-
cg.close();
75-
});
76-
7762
it('should throw if already initialized', () => {
7863
const cg = CodeGraph.initSync(tempDir);
7964
cg.close();
8065

8166
expect(() => CodeGraph.initSync(tempDir)).toThrow(/already initialized/i);
8267
});
83-
84-
it('should accept custom config options', () => {
85-
const cg = CodeGraph.initSync(tempDir, {
86-
config: {
87-
maxFileSize: 500000,
88-
extractDocstrings: false,
89-
},
90-
});
91-
92-
const config = cg.getConfig();
93-
expect(config.maxFileSize).toBe(500000);
94-
expect(config.extractDocstrings).toBe(false);
95-
96-
cg.close();
97-
});
9868
});
9969

10070
describe('Opening Projects', () => {
@@ -112,17 +82,6 @@ describe('CodeGraph Foundation', () => {
11282
it('should throw if not initialized', () => {
11383
expect(() => CodeGraph.openSync(tempDir)).toThrow(/not initialized/i);
11484
});
115-
116-
it('should preserve configuration across open/close', () => {
117-
const cg1 = CodeGraph.initSync(tempDir, {
118-
config: { maxFileSize: 123456 },
119-
});
120-
cg1.close();
121-
122-
const cg2 = CodeGraph.openSync(tempDir);
123-
expect(cg2.getConfig().maxFileSize).toBe(123456);
124-
cg2.close();
125-
});
12685
});
12786

12887
describe('Static Methods', () => {
@@ -182,31 +141,6 @@ describe('CodeGraph Foundation', () => {
182141
});
183142
});
184143

185-
describe('Configuration', () => {
186-
it('should load and merge config with defaults', () => {
187-
const cg = CodeGraph.initSync(tempDir);
188-
cg.close();
189-
190-
const config = loadConfig(tempDir);
191-
expect(config.version).toBe(DEFAULT_CONFIG.version);
192-
expect(config.rootDir).toBe(path.resolve(tempDir));
193-
});
194-
195-
it('should update configuration', () => {
196-
const cg = CodeGraph.initSync(tempDir);
197-
198-
cg.updateConfig({ maxFileSize: 999999 });
199-
200-
expect(cg.getConfig().maxFileSize).toBe(999999);
201-
202-
cg.close();
203-
204-
// Verify persistence
205-
const config = loadConfig(tempDir);
206-
expect(config.maxFileSize).toBe(999999);
207-
});
208-
});
209-
210144
describe('Directory Management', () => {
211145
it('should validate directory structure', () => {
212146
const cg = CodeGraph.initSync(tempDir);

0 commit comments

Comments
 (0)