Skip to content
This repository was archived by the owner on Oct 10, 2025. It is now read-only.

Commit 13221b0

Browse files
committed
fix: add contextignore recursively
1 parent f7ca04c commit 13221b0

File tree

6 files changed

+510
-104
lines changed

6 files changed

+510
-104
lines changed

linters/typescript/README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,37 @@ Replace `<directory_to_lint>` with the path to the directory containing your Cod
5858
- 🔍 Validates the structure and content of `.context.md`, `.context.yaml`, and `.context.json` files
5959
- ✅ Checks for required fields and sections
6060
- 📄 Verifies the format of `.contextdocs.md` files
61-
- 🚫 Validates ignore patterns in `.contextignore` files
61+
- 🚫 Supports and validates `.contextignore` files for excluding specific files or directories
6262
- 💬 Provides detailed error messages and warnings
6363

64+
## 📁 .contextignore Files
65+
66+
`.contextignore` files allow you to specify patterns for files and directories that should be ignored by the Codebase Context Lint. This is useful for excluding generated files, dependencies, or any other content that doesn't need context documentation.
67+
68+
### How to use .contextignore
69+
70+
1. Create a file named `.contextignore` in any directory of your project.
71+
2. Add patterns to the file, one per line. These patterns follow the same rules as `.gitignore` files.
72+
3. The linter will respect these ignore patterns when processing files in that directory and its subdirectories.
73+
74+
Example `.contextignore` file:
75+
76+
```
77+
# Ignore node_modules directory
78+
node_modules/
79+
80+
# Ignore all .log files
81+
*.log
82+
83+
# Ignore a specific file
84+
path/to/specific/file.js
85+
86+
# Ignore all files in a specific directory
87+
path/to/ignore/*
88+
```
89+
90+
The linter will validate the syntax of your `.contextignore` files and warn about any problematic patterns, such as attempting to ignore critical context files.
91+
6492
## 🤖 Using with AI Assistants
6593

6694
While this linter provides automated validation of CCS files, you can also use the Codebase Context Specification with AI assistants without any specific tooling. The [CODING-ASSISTANT-PROMPT.md](https://github.com/Agentic-Insights/codebase-context-spec/blob/main/CODING-ASSISTANT-PROMPT.md) file in the main repository provides guidelines for AI assistants to understand and use the Codebase Context Specification.
@@ -71,7 +99,7 @@ To use the Codebase Context Specification with an AI assistant:
7199
2. Ask the AI to analyze your project's context files based on these guidelines.
72100
3. The AI will be able to provide more accurate and context-aware responses by following the instructions in the prompt.
73101

74-
Note that while this approach allows for immediate use of the specification, some features like .contextignore should eventually be applied by tooling (such as this linter) for more robust implementation.
102+
Note that while this approach allows for immediate use of the specification, some features like .contextignore are best implemented by tooling (such as this linter) for more robust and consistent application.
75103

76104
## 🛠️ Development
77105

@@ -104,11 +132,11 @@ To contribute to this project:
104132
7. Push to the branch (`git push origin feature/AmazingFeature`)
105133
8. Open a Pull Request
106134

107-
## Learn More
135+
## 📖 Learn More
108136

109137
For a deeper dive into the Codebase Context Specification, check out this [SubStack article by Vaskin](https://agenticinsights.substack.com/p/codebase-context-specification-rfc), the author of the specification.
110138

111-
## 📄 License
139+
## 📄 License
112140

113141
This project is licensed under the MIT License.
114142

linters/typescript/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,17 @@
3737
"@types/jest": "^29.5.12",
3838
"@types/js-yaml": "^4.0.9",
3939
"@types/markdown-it": "^14.1.2",
40-
"@types/node": "^22.5.1",
40+
"@types/node": "^20.11.19",
4141
"jest": "^29.7.0",
42-
"semantic-release": "^22.0.12",
42+
"semantic-release": "^23.0.2",
4343
"ts-jest": "^29.1.2",
44-
"typescript": "^5.5.4"
44+
"typescript": "^5.3.3"
4545
},
4646
"dependencies": {
4747
"gray-matter": "^4.0.3",
48+
"ignore": "^5.3.1",
4849
"js-yaml": "^4.1.0",
49-
"markdown-it": "^14.1.0"
50+
"markdown-it": "^14.0.0"
5051
},
5152
"engines": {
5253
"node": ">=20.0.0"
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ContextLinter } from '../context_linter';
2+
import { ContextignoreLinter } from '../contextignore_linter';
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
6+
jest.mock('fs', () => ({
7+
promises: {
8+
readdir: jest.fn(),
9+
readFile: jest.fn(),
10+
},
11+
}));
12+
13+
jest.mock('../contextignore_linter');
14+
15+
describe('ContextLinter', () => {
16+
let linter: ContextLinter;
17+
let mockContextignoreLinter: jest.Mocked<ContextignoreLinter>;
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
mockContextignoreLinter = new ContextignoreLinter() as jest.Mocked<ContextignoreLinter>;
22+
linter = new ContextLinter();
23+
(linter as any).contextignoreLinter = mockContextignoreLinter;
24+
});
25+
26+
describe('lintDirectory', () => {
27+
it('should lint a directory successfully', async () => {
28+
const mockFiles = [
29+
{ name: '.contextignore', isDirectory: () => false },
30+
{ name: '.context.md', isDirectory: () => false },
31+
{ name: 'subdir', isDirectory: () => true },
32+
];
33+
34+
(fs.promises.readdir as jest.Mock).mockResolvedValue(mockFiles);
35+
(fs.promises.readFile as jest.Mock).mockResolvedValue('# Mock content');
36+
mockContextignoreLinter.lintContextignoreFile.mockResolvedValue(true);
37+
mockContextignoreLinter.isIgnored.mockReturnValue(false);
38+
39+
const result = await linter.lintDirectory('/mock/path', '1.0.0');
40+
41+
expect(result).toBe(true);
42+
expect(mockContextignoreLinter.lintContextignoreFile).toHaveBeenCalled();
43+
expect(mockContextignoreLinter.isIgnored).toHaveBeenCalled();
44+
});
45+
46+
it('should respect .contextignore rules', async () => {
47+
const mockFiles = [
48+
{ name: '.contextignore', isDirectory: () => false },
49+
{ name: 'ignored.md', isDirectory: () => false },
50+
{ name: 'not_ignored.md', isDirectory: () => false },
51+
];
52+
53+
(fs.promises.readdir as jest.Mock).mockResolvedValue(mockFiles);
54+
(fs.promises.readFile as jest.Mock).mockResolvedValue('# Mock content');
55+
mockContextignoreLinter.lintContextignoreFile.mockResolvedValue(true);
56+
mockContextignoreLinter.isIgnored
57+
.mockReturnValueOnce(false) // .contextignore
58+
.mockReturnValueOnce(true) // ignored.md
59+
.mockReturnValueOnce(false); // not_ignored.md
60+
61+
await linter.lintDirectory('/mock/path', '1.0.0');
62+
63+
expect(mockContextignoreLinter.isIgnored).toHaveBeenCalledTimes(3);
64+
// Ensure that lintContextFile is not called for ignored.md
65+
expect((linter as any).lintContextFile).not.toHaveBeenCalledWith(expect.stringContaining('ignored.md'));
66+
});
67+
});
68+
69+
describe('handleContextignoreRecursively', () => {
70+
it('should process .contextignore files in nested directories', async () => {
71+
const mockStructure = [
72+
{ name: '.contextignore', isDirectory: () => false },
73+
{ name: 'subdir', isDirectory: () => true },
74+
];
75+
const mockSubdirStructure = [
76+
{ name: '.contextignore', isDirectory: () => false },
77+
];
78+
79+
(fs.promises.readdir as jest.Mock)
80+
.mockResolvedValueOnce(mockStructure)
81+
.mockResolvedValueOnce(mockSubdirStructure);
82+
(fs.promises.readFile as jest.Mock).mockResolvedValue('# Mock content');
83+
mockContextignoreLinter.lintContextignoreFile.mockResolvedValue(true);
84+
85+
await (linter as any).handleContextignoreRecursively('/mock/path');
86+
87+
expect(mockContextignoreLinter.lintContextignoreFile).toHaveBeenCalledTimes(2);
88+
});
89+
});
90+
91+
// Add more tests for other methods as needed
92+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ContextignoreLinter } from '../contextignore_linter';
2+
import * as path from 'path';
3+
4+
describe('ContextignoreLinter', () => {
5+
let linter: ContextignoreLinter;
6+
7+
beforeEach(() => {
8+
linter = new ContextignoreLinter();
9+
});
10+
11+
describe('lintContextignoreFile', () => {
12+
it('should validate a correct .contextignore file', async () => {
13+
const content = `
14+
# This is a comment
15+
node_modules/
16+
*.log
17+
/build
18+
`;
19+
const result = await linter.lintContextignoreFile(content, 'test/.contextignore');
20+
expect(result).toBe(true);
21+
});
22+
23+
it('should detect invalid patterns', async () => {
24+
const content = `
25+
node_modules/
26+
*.log
27+
/build
28+
invalid**pattern
29+
`;
30+
const result = await linter.lintContextignoreFile(content, 'test/.contextignore');
31+
expect(result).toBe(false);
32+
});
33+
34+
it('should detect attempts to ignore critical files', async () => {
35+
const content = `
36+
node_modules/
37+
*.log
38+
.context.md
39+
`;
40+
const result = await linter.lintContextignoreFile(content, 'test/.contextignore');
41+
expect(result).toBe(false);
42+
});
43+
44+
it('should detect conflicting patterns', async () => {
45+
const content = `
46+
node_modules/
47+
!node_modules/important/
48+
node_modules/important/
49+
`;
50+
const result = await linter.lintContextignoreFile(content, 'test/.contextignore');
51+
expect(result).toBe(false);
52+
});
53+
});
54+
55+
describe('isIgnored', () => {
56+
beforeEach(async () => {
57+
const content = `
58+
node_modules/
59+
*.log
60+
/build
61+
`;
62+
await linter.lintContextignoreFile(content, '/project/.contextignore');
63+
});
64+
65+
it('should correctly identify ignored files', () => {
66+
expect(linter.isIgnored('/project/node_modules/package/index.js', '/project')).toBe(true);
67+
expect(linter.isIgnored('/project/app.log', '/project')).toBe(true);
68+
expect(linter.isIgnored('/project/build/main.js', '/project')).toBe(true);
69+
});
70+
71+
it('should correctly identify non-ignored files', () => {
72+
expect(linter.isIgnored('/project/src/index.js', '/project')).toBe(false);
73+
expect(linter.isIgnored('/project/README.md', '/project')).toBe(false);
74+
});
75+
76+
it('should not ignore critical files', () => {
77+
expect(linter.isIgnored('/project/.context.md', '/project')).toBe(false);
78+
expect(linter.isIgnored('/project/subdir/.context.yaml', '/project')).toBe(false);
79+
expect(linter.isIgnored('/project/.contextignore', '/project')).toBe(false);
80+
});
81+
});
82+
83+
describe('getIgnoredFiles', () => {
84+
beforeEach(async () => {
85+
const content = `
86+
*.log
87+
/build
88+
`;
89+
await linter.lintContextignoreFile(content, '/project/.contextignore');
90+
});
91+
92+
it('should return a list of ignored files', () => {
93+
const ignoredFiles = linter.getIgnoredFiles('/project');
94+
expect(ignoredFiles).toContain('app.log');
95+
expect(ignoredFiles).toContain('build/main.js');
96+
expect(ignoredFiles).not.toContain('src/index.js');
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)