mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat: add .understandignore support for user-configurable file exclusion
This commit is contained in:
@@ -0,0 +1,776 @@
|
||||
# .understandignore Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add user-configurable file exclusion via `.understandignore` files using `.gitignore` syntax, with auto-generated starter files and a pre-analysis review pause.
|
||||
|
||||
**Architecture:** An `IgnoreFilter` module in `packages/core` uses the `ignore` npm package to parse `.understandignore` files and filter paths. A companion `IgnoreGenerator` scans the project for common patterns and produces a commented-out starter file. The `project-scanner` agent applies the filter as a second pass after its existing hardcoded exclusions. The `/understand` skill adds a Phase 0.5 that generates the starter file and pauses for user review.
|
||||
|
||||
**Tech Stack:** TypeScript, `ignore` npm package, Vitest
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-10-understandignore-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Core package
|
||||
- Create: `understand-anything-plugin/packages/core/src/ignore-filter.ts` — parse .understandignore, merge with defaults, filter paths
|
||||
- Create: `understand-anything-plugin/packages/core/src/ignore-generator.ts` — generate starter .understandignore by scanning project
|
||||
- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts` — filter tests
|
||||
- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts` — generator tests
|
||||
- Modify: `understand-anything-plugin/packages/core/src/index.ts` — export new modules
|
||||
- Modify: `understand-anything-plugin/packages/core/package.json` — add `ignore` dependency
|
||||
|
||||
### Agents & skills
|
||||
- Modify: `understand-anything-plugin/agents/project-scanner.md` — add Layer 2 filtering step
|
||||
- Modify: `understand-anything-plugin/skills/understand/SKILL.md` — add Phase 0.5
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add `ignore` dependency
|
||||
|
||||
**Files:**
|
||||
- Modify: `understand-anything-plugin/packages/core/package.json`
|
||||
|
||||
- [ ] **Step 1: Install the `ignore` npm package**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd understand-anything-plugin && pnpm add --filter @understand-anything/core ignore
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify it was added**
|
||||
|
||||
Run: `grep ignore understand-anything-plugin/packages/core/package.json`
|
||||
Expected: `"ignore": "^7.x.x"` (or similar) in dependencies
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add understand-anything-plugin/packages/core/package.json understand-anything-plugin/pnpm-lock.yaml
|
||||
git commit -m "chore(core): add ignore package for .understandignore support"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Create IgnoreFilter module with tests (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `understand-anything-plugin/packages/core/src/ignore-filter.ts`
|
||||
- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createIgnoreFilter, DEFAULT_IGNORE_PATTERNS } from "../ignore-filter";
|
||||
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
describe("IgnoreFilter", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `ignore-filter-test-${Date.now()}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
mkdirSync(join(testDir, ".understand-anything"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("DEFAULT_IGNORE_PATTERNS", () => {
|
||||
it("contains node_modules", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("node_modules/");
|
||||
});
|
||||
|
||||
it("contains .git", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain(".git/");
|
||||
});
|
||||
|
||||
it("contains bin and obj for .NET", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("bin/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("obj/");
|
||||
});
|
||||
|
||||
it("contains build output directories", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("dist/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("build/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("out/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("coverage/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createIgnoreFilter with no user file", () => {
|
||||
it("ignores files matching default patterns", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("node_modules/foo/bar.js")).toBe(true);
|
||||
expect(filter.isIgnored("dist/index.js")).toBe(true);
|
||||
expect(filter.isIgnored(".git/config")).toBe(true);
|
||||
expect(filter.isIgnored("bin/Debug/app.dll")).toBe(true);
|
||||
expect(filter.isIgnored("obj/Release/net8.0/app.dll")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not ignore source files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
expect(filter.isIgnored("README.md")).toBe(false);
|
||||
expect(filter.isIgnored("package.json")).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores lock files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("pnpm-lock.yaml")).toBe(true);
|
||||
expect(filter.isIgnored("package-lock.json")).toBe(true);
|
||||
expect(filter.isIgnored("yarn.lock")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores binary/asset files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("logo.png")).toBe(true);
|
||||
expect(filter.isIgnored("font.woff2")).toBe(true);
|
||||
expect(filter.isIgnored("doc.pdf")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores generated files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("bundle.min.js")).toBe(true);
|
||||
expect(filter.isIgnored("style.min.css")).toBe(true);
|
||||
expect(filter.isIgnored("source.map")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores IDE directories", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored(".idea/workspace.xml")).toBe(true);
|
||||
expect(filter.isIgnored(".vscode/settings.json")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createIgnoreFilter with user .understandignore", () => {
|
||||
it("reads patterns from .understand-anything/.understandignore", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"# Exclude tests\n__tests__/\n*.test.ts\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("__tests__/foo.test.ts")).toBe(true);
|
||||
expect(filter.isIgnored("src/utils.test.ts")).toBe(true);
|
||||
expect(filter.isIgnored("src/utils.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("reads patterns from project root .understandignore", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understandignore"),
|
||||
"docs/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("docs/README.md")).toBe(true);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles # comments and blank lines", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"# This is a comment\n\n\nfixtures/\n\n# Another comment\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("fixtures/data.json")).toBe(true);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("supports ! negation to override defaults", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"!dist/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
// dist/ is in defaults but negated by user
|
||||
expect(filter.isIgnored("dist/index.js")).toBe(false);
|
||||
});
|
||||
|
||||
it("supports ** recursive matching", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"**/snapshots/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("src/components/snapshots/Button.snap")).toBe(true);
|
||||
expect(filter.isIgnored("snapshots/foo.snap")).toBe(true);
|
||||
});
|
||||
|
||||
it("merges .understand-anything/ and root .understandignore", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"__tests__/\n"
|
||||
);
|
||||
writeFileSync(
|
||||
join(testDir, ".understandignore"),
|
||||
"fixtures/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("__tests__/foo.ts")).toBe(true);
|
||||
expect(filter.isIgnored("fixtures/data.json")).toBe(true);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-filter.test.ts`
|
||||
Expected: FAIL — module not found
|
||||
|
||||
- [ ] **Step 3: Implement IgnoreFilter**
|
||||
|
||||
Create `understand-anything-plugin/packages/core/src/ignore-filter.ts`:
|
||||
|
||||
```typescript
|
||||
import ignore, { type Ignore } from "ignore";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Hardcoded default ignore patterns matching the project-scanner agent's
|
||||
* exclusion rules, plus bin/obj for .NET projects.
|
||||
*/
|
||||
export const DEFAULT_IGNORE_PATTERNS: string[] = [
|
||||
// Dependency directories
|
||||
"node_modules/",
|
||||
".git/",
|
||||
"vendor/",
|
||||
"venv/",
|
||||
".venv/",
|
||||
"__pycache__/",
|
||||
|
||||
// Build output
|
||||
"dist/",
|
||||
"build/",
|
||||
"out/",
|
||||
"coverage/",
|
||||
".next/",
|
||||
".cache/",
|
||||
".turbo/",
|
||||
"target/",
|
||||
"bin/",
|
||||
"obj/",
|
||||
|
||||
// Lock files
|
||||
"*.lock",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
|
||||
// Binary/asset files
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.gif",
|
||||
"*.svg",
|
||||
"*.ico",
|
||||
"*.woff",
|
||||
"*.woff2",
|
||||
"*.ttf",
|
||||
"*.eot",
|
||||
"*.mp3",
|
||||
"*.mp4",
|
||||
"*.pdf",
|
||||
"*.zip",
|
||||
"*.tar",
|
||||
"*.gz",
|
||||
|
||||
// Generated files
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
"*.map",
|
||||
"*.generated.*",
|
||||
|
||||
// IDE/editor
|
||||
".idea/",
|
||||
".vscode/",
|
||||
|
||||
// Misc
|
||||
"LICENSE",
|
||||
".gitignore",
|
||||
".editorconfig",
|
||||
".prettierrc",
|
||||
".eslintrc*",
|
||||
"*.log",
|
||||
];
|
||||
|
||||
export interface IgnoreFilter {
|
||||
/** Returns true if the given relative path should be excluded from analysis. */
|
||||
isIgnored(relativePath: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an IgnoreFilter that merges hardcoded defaults with user-defined
|
||||
* patterns from .understandignore files.
|
||||
*
|
||||
* Pattern load order (later entries can override earlier ones via ! negation):
|
||||
* 1. Hardcoded defaults
|
||||
* 2. .understand-anything/.understandignore (if exists)
|
||||
* 3. .understandignore at project root (if exists)
|
||||
*/
|
||||
export function createIgnoreFilter(projectRoot: string): IgnoreFilter {
|
||||
const ig: Ignore = ignore();
|
||||
|
||||
// Layer 1: hardcoded defaults
|
||||
ig.add(DEFAULT_IGNORE_PATTERNS);
|
||||
|
||||
// Layer 2: .understand-anything/.understandignore
|
||||
const projectIgnorePath = join(projectRoot, ".understand-anything", ".understandignore");
|
||||
if (existsSync(projectIgnorePath)) {
|
||||
const content = readFileSync(projectIgnorePath, "utf-8");
|
||||
ig.add(content);
|
||||
}
|
||||
|
||||
// Layer 3: .understandignore at project root
|
||||
const rootIgnorePath = join(projectRoot, ".understandignore");
|
||||
if (existsSync(rootIgnorePath)) {
|
||||
const content = readFileSync(rootIgnorePath, "utf-8");
|
||||
ig.add(content);
|
||||
}
|
||||
|
||||
return {
|
||||
isIgnored(relativePath: string): boolean {
|
||||
return ig.ignores(relativePath);
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-filter.test.ts`
|
||||
Expected: All tests PASS
|
||||
|
||||
- [ ] **Step 5: Build to verify no type errors**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core build`
|
||||
Expected: Clean build
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add understand-anything-plugin/packages/core/src/ignore-filter.ts understand-anything-plugin/packages/core/src/__tests__/ignore-filter.test.ts
|
||||
git commit -m "feat(core): add IgnoreFilter module with .understandignore parsing and tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Create IgnoreGenerator module with tests (TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `understand-anything-plugin/packages/core/src/ignore-generator.ts`
|
||||
- Create: `understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { generateStarterIgnoreFile } from "../ignore-generator";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
describe("generateStarterIgnoreFile", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `ignore-gen-test-${Date.now()}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("includes a header comment explaining the file", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain(".understandignore");
|
||||
expect(content).toContain("same as .gitignore");
|
||||
expect(content).toContain("Built-in defaults");
|
||||
});
|
||||
|
||||
it("all suggestions are commented out", () => {
|
||||
// Create some directories to trigger suggestions
|
||||
mkdirSync(join(testDir, "__tests__"), { recursive: true });
|
||||
mkdirSync(join(testDir, "docs"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
||||
// No active (uncommented) patterns
|
||||
expect(lines).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("suggests __tests__ when __tests__ directory exists", () => {
|
||||
mkdirSync(join(testDir, "__tests__"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# __tests__/");
|
||||
});
|
||||
|
||||
it("suggests docs when docs directory exists", () => {
|
||||
mkdirSync(join(testDir, "docs"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# docs/");
|
||||
});
|
||||
|
||||
it("suggests test directories when they exist", () => {
|
||||
mkdirSync(join(testDir, "test"), { recursive: true });
|
||||
mkdirSync(join(testDir, "tests"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# test/");
|
||||
expect(content).toContain("# tests/");
|
||||
});
|
||||
|
||||
it("suggests fixtures when fixtures directory exists", () => {
|
||||
mkdirSync(join(testDir, "fixtures"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# fixtures/");
|
||||
});
|
||||
|
||||
it("suggests examples when examples directory exists", () => {
|
||||
mkdirSync(join(testDir, "examples"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# examples/");
|
||||
});
|
||||
|
||||
it("suggests .storybook when .storybook directory exists", () => {
|
||||
mkdirSync(join(testDir, ".storybook"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# .storybook/");
|
||||
});
|
||||
|
||||
it("suggests migrations when migrations directory exists", () => {
|
||||
mkdirSync(join(testDir, "migrations"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# migrations/");
|
||||
});
|
||||
|
||||
it("suggests scripts when scripts directory exists", () => {
|
||||
mkdirSync(join(testDir, "scripts"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# scripts/");
|
||||
});
|
||||
|
||||
it("always includes generic suggestions", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# *.snap");
|
||||
expect(content).toContain("# *.test.*");
|
||||
expect(content).toContain("# *.spec.*");
|
||||
});
|
||||
|
||||
it("does not suggest directories that don't exist", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
// __tests__ doesn't exist, so it shouldn't be in directory suggestions
|
||||
// (it may still be in generic test file patterns)
|
||||
expect(content).not.toContain("# __tests__/");
|
||||
expect(content).not.toContain("# .storybook/");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-generator.test.ts`
|
||||
Expected: FAIL — module not found
|
||||
|
||||
- [ ] **Step 3: Implement IgnoreGenerator**
|
||||
|
||||
Create `understand-anything-plugin/packages/core/src/ignore-generator.ts`:
|
||||
|
||||
```typescript
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const HEADER = `# .understandignore — patterns for files/dirs to exclude from analysis
|
||||
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
|
||||
# Lines below are suggestions — uncomment to activate.
|
||||
# Use ! prefix to force-include something excluded by defaults.
|
||||
#
|
||||
# Built-in defaults (always excluded unless negated):
|
||||
# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.
|
||||
#
|
||||
`;
|
||||
|
||||
/** Directories to check for and suggest excluding. */
|
||||
const DETECTABLE_DIRS = [
|
||||
{ dir: "__tests__", pattern: "__tests__/" },
|
||||
{ dir: "test", pattern: "test/" },
|
||||
{ dir: "tests", pattern: "tests/" },
|
||||
{ dir: "fixtures", pattern: "fixtures/" },
|
||||
{ dir: "testdata", pattern: "testdata/" },
|
||||
{ dir: "docs", pattern: "docs/" },
|
||||
{ dir: "examples", pattern: "examples/" },
|
||||
{ dir: "scripts", pattern: "scripts/" },
|
||||
{ dir: "migrations", pattern: "migrations/" },
|
||||
{ dir: ".storybook", pattern: ".storybook/" },
|
||||
];
|
||||
|
||||
/** Always-included generic suggestions. */
|
||||
const GENERIC_SUGGESTIONS = [
|
||||
"*.test.*",
|
||||
"*.spec.*",
|
||||
"*.snap",
|
||||
];
|
||||
|
||||
/**
|
||||
* Generates a starter .understandignore file by scanning the project root
|
||||
* for common directories and suggesting them as commented-out exclusions.
|
||||
*
|
||||
* All suggestions are commented out — the user must uncomment to activate.
|
||||
* Returns the file content as a string.
|
||||
*/
|
||||
export function generateStarterIgnoreFile(projectRoot: string): string {
|
||||
const sections: string[] = [HEADER];
|
||||
|
||||
// Detected directory suggestions
|
||||
const detected: string[] = [];
|
||||
for (const { dir, pattern } of DETECTABLE_DIRS) {
|
||||
if (existsSync(join(projectRoot, dir))) {
|
||||
detected.push(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (detected.length > 0) {
|
||||
sections.push("# --- Detected directories (uncomment to exclude) ---\n");
|
||||
for (const pattern of detected) {
|
||||
sections.push(`# ${pattern}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Generic suggestions (always included)
|
||||
sections.push("# --- Test file patterns (uncomment to exclude) ---\n");
|
||||
for (const pattern of GENERIC_SUGGESTIONS) {
|
||||
sections.push(`# ${pattern}`);
|
||||
}
|
||||
sections.push("");
|
||||
|
||||
return sections.join("\n");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify they pass**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core test -- --run src/__tests__/ignore-generator.test.ts`
|
||||
Expected: All tests PASS
|
||||
|
||||
- [ ] **Step 5: Build**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core build`
|
||||
Expected: Clean build
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add understand-anything-plugin/packages/core/src/ignore-generator.ts understand-anything-plugin/packages/core/src/__tests__/ignore-generator.test.ts
|
||||
git commit -m "feat(core): add IgnoreGenerator for starter .understandignore file creation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Export new modules from core
|
||||
|
||||
**Files:**
|
||||
- Modify: `understand-anything-plugin/packages/core/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Add exports**
|
||||
|
||||
Add to the end of `understand-anything-plugin/packages/core/src/index.ts`:
|
||||
|
||||
```typescript
|
||||
export {
|
||||
createIgnoreFilter,
|
||||
DEFAULT_IGNORE_PATTERNS,
|
||||
type IgnoreFilter,
|
||||
} from "./ignore-filter.js";
|
||||
export { generateStarterIgnoreFile } from "./ignore-generator.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build and run all tests**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core build && pnpm --filter @understand-anything/core test -- --run`
|
||||
Expected: Clean build, all tests pass
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add understand-anything-plugin/packages/core/src/index.ts
|
||||
git commit -m "feat(core): export IgnoreFilter and IgnoreGenerator from core index"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update project-scanner agent
|
||||
|
||||
**Files:**
|
||||
- Modify: `understand-anything-plugin/agents/project-scanner.md`
|
||||
|
||||
- [ ] **Step 1: Read the current project-scanner.md**
|
||||
|
||||
Read `understand-anything-plugin/agents/project-scanner.md` to understand the current structure.
|
||||
|
||||
- [ ] **Step 2: Add bin/ and obj/ to hardcoded exclusions**
|
||||
|
||||
In Step 2 (Exclusion Filtering), add `bin/` and `obj/` to the "Build output" line:
|
||||
|
||||
Change:
|
||||
```
|
||||
- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust)
|
||||
```
|
||||
|
||||
To:
|
||||
```
|
||||
- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust), `bin/` (.NET), `obj/` (.NET)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add Layer 2 filtering step**
|
||||
|
||||
After Step 2 (Exclusion Filtering), add a new step:
|
||||
|
||||
```markdown
|
||||
**Step 2.5 -- User-Configured Filtering (.understandignore)**
|
||||
|
||||
After applying the hardcoded exclusion filters above, apply user-configured patterns from `.understandignore`:
|
||||
|
||||
1. Check if `.understand-anything/.understandignore` exists in the project root. If so, read it.
|
||||
2. Check if `.understandignore` exists in the project root. If so, read it.
|
||||
3. Parse both files using `.gitignore` syntax (glob patterns, `#` comments, blank lines ignored, `!` prefix for negation, trailing `/` for directories, `**/` for recursive matching).
|
||||
4. Filter the remaining file list through these patterns. Files matching any pattern are excluded.
|
||||
5. `!` negation patterns override the hardcoded exclusions from Step 2 (e.g., `!dist/` force-includes dist/).
|
||||
6. Track the count of files removed by this step as `filteredByIgnore`.
|
||||
|
||||
This filtering must be deterministic (not LLM-based). Use a Node.js script with the `ignore` npm package if implementing programmatically, or apply the patterns manually if the file list is small.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update scan output schema**
|
||||
|
||||
Find the output JSON schema section and add `filteredByIgnore` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "...",
|
||||
"description": "...",
|
||||
"languages": ["..."],
|
||||
"frameworks": ["..."],
|
||||
"files": [...],
|
||||
"totalFiles": 123,
|
||||
"filteredByIgnore": 5,
|
||||
"estimatedComplexity": "moderate",
|
||||
"importMap": {}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add understand-anything-plugin/agents/project-scanner.md
|
||||
git commit -m "feat(agent): add .understandignore support and bin/obj exclusions to project-scanner"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update /understand skill with Phase 0.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `understand-anything-plugin/skills/understand/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Read the current SKILL.md Phase 0 section**
|
||||
|
||||
Read `understand-anything-plugin/skills/understand/SKILL.md` lines 22-80 to understand Phase 0.
|
||||
|
||||
- [ ] **Step 2: Add Phase 0.5 after Phase 0**
|
||||
|
||||
After the Phase 0 section (after the `---` separator before Phase 1), insert:
|
||||
|
||||
```markdown
|
||||
## Phase 0.5 — Ignore Configuration
|
||||
|
||||
Set up and verify the `.understandignore` file before scanning.
|
||||
|
||||
1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists.
|
||||
2. **If it does NOT exist**, generate a starter file:
|
||||
- Run a Node.js script (or inline logic) that scans `$PROJECT_ROOT` for common directories (`__tests__/`, `test/`, `tests/`, `fixtures/`, `testdata/`, `docs/`, `examples/`, `scripts/`, `migrations/`, `.storybook/`) and generates a `.understandignore` file with commented-out suggestions.
|
||||
- Write the generated content to `$PROJECT_ROOT/.understand-anything/.understandignore`.
|
||||
- Report to the user:
|
||||
> "Generated `.understand-anything/.understandignore` with suggested exclusions based on your project structure. Please review it and uncomment any patterns you'd like to exclude from analysis. When ready, confirm to continue."
|
||||
- **Wait for user confirmation before proceeding.**
|
||||
3. **If it already exists**, report:
|
||||
> "Found `.understand-anything/.understandignore`. Review it if needed, then confirm to continue."
|
||||
- **Wait for user confirmation before proceeding.**
|
||||
4. After confirmation, proceed to Phase 1.
|
||||
|
||||
**Note:** The `.understandignore` file uses `.gitignore` syntax. The user can add patterns to exclude files from analysis, or use `!` prefix to force-include files excluded by built-in defaults (e.g., `!dist/` to analyze dist/ files).
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update Phase 1 reporting**
|
||||
|
||||
In the Phase 1 section, after the gate check (~line 114), add a note about reporting ignore stats:
|
||||
|
||||
```markdown
|
||||
After scanning, if the scan result includes `filteredByIgnore > 0`, report:
|
||||
> "Scanned {totalFiles} files ({filteredByIgnore} excluded by .understandignore)"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add understand-anything-plugin/skills/understand/SKILL.md
|
||||
git commit -m "feat(skill): add Phase 0.5 for .understandignore setup and review pause"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Build, test, and verify end-to-end
|
||||
|
||||
**Files:**
|
||||
- All modified files
|
||||
|
||||
- [ ] **Step 1: Build core**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core build`
|
||||
Expected: Clean build
|
||||
|
||||
- [ ] **Step 2: Run all core tests**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/core test -- --run`
|
||||
Expected: All tests pass (existing + new ignore-filter + ignore-generator tests)
|
||||
|
||||
- [ ] **Step 3: Build skill package**
|
||||
|
||||
Run: `pnpm --filter @understand-anything/skill build`
|
||||
Expected: Clean build
|
||||
|
||||
- [ ] **Step 4: Verify files exist**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
ls understand-anything-plugin/packages/core/src/ignore-filter.ts understand-anything-plugin/packages/core/src/ignore-generator.ts
|
||||
```
|
||||
Expected: Both files listed
|
||||
|
||||
- [ ] **Step 5: Verify exports work**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
node -e "import('@understand-anything/core').then(m => { console.log('IgnoreFilter:', typeof m.createIgnoreFilter); console.log('Generator:', typeof m.generateStarterIgnoreFile); })"
|
||||
```
|
||||
Expected: Both show `function`
|
||||
|
||||
- [ ] **Step 6: Final commit (if any unstaged changes)**
|
||||
|
||||
```bash
|
||||
git status
|
||||
# If clean, skip. If changes exist:
|
||||
git add -A && git commit -m "chore: final verification for .understandignore support"
|
||||
```
|
||||
@@ -0,0 +1,258 @@
|
||||
# .understandignore Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
Add user-configurable file exclusion via `.understandignore` files, using `.gitignore` syntax. This makes analysis faster by skipping irrelevant files (vendor code, generated output, test fixtures) without modifying hardcoded defaults.
|
||||
|
||||
## Goals
|
||||
|
||||
- Let users exclude files/directories from analysis via `.understandignore`
|
||||
- Use `.gitignore` syntax (familiar, no learning curve)
|
||||
- Keep hardcoded defaults as built-in — `.understandignore` adds patterns on top
|
||||
- Allow `!` negation to force-include files excluded by defaults
|
||||
- Auto-generate a commented-out starter file on first run (deterministic code, not LLM)
|
||||
- Pause before analysis to let user review the ignore file
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Replacing `.gitignore` — this is analysis-specific
|
||||
- Per-directory `.understandignore` files (project root and `.understand-anything/` only)
|
||||
- GUI for editing ignore patterns
|
||||
|
||||
---
|
||||
|
||||
## IgnoreFilter Module
|
||||
|
||||
New file: `packages/core/src/ignore-filter.ts`
|
||||
|
||||
Uses the [`ignore`](https://www.npmjs.com/package/ignore) npm package for gitignore-compatible pattern matching.
|
||||
|
||||
### API
|
||||
|
||||
```typescript
|
||||
export interface IgnoreFilter {
|
||||
isIgnored(relativePath: string): boolean;
|
||||
}
|
||||
|
||||
export function createIgnoreFilter(projectRoot: string): IgnoreFilter;
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
`createIgnoreFilter` loads patterns in this order (later entries can override earlier ones):
|
||||
|
||||
1. **Hardcoded defaults** — the existing exclusion patterns from project-scanner (node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.)
|
||||
2. **`.understand-anything/.understandignore`** — project-level, lives alongside the output
|
||||
3. **`.understandignore`** at project root — alternative location for visibility
|
||||
|
||||
Patterns merge additively. `!` negation in user files can override hardcoded defaults (e.g., `!dist/` force-includes dist/).
|
||||
|
||||
### Hardcoded Default Patterns
|
||||
|
||||
These are the built-in defaults (matching current project-scanner behavior, plus bin/obj for .NET):
|
||||
|
||||
```
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
.git/
|
||||
vendor/
|
||||
venv/
|
||||
.venv/
|
||||
__pycache__/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
coverage/
|
||||
.next/
|
||||
.cache/
|
||||
.turbo/
|
||||
target/
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Lock files
|
||||
*.lock
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Binary/asset files
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.svg
|
||||
*.ico
|
||||
*.woff
|
||||
*.woff2
|
||||
*.ttf
|
||||
*.eot
|
||||
*.mp3
|
||||
*.mp4
|
||||
*.pdf
|
||||
*.zip
|
||||
*.tar
|
||||
*.gz
|
||||
|
||||
# Generated files
|
||||
*.min.js
|
||||
*.min.css
|
||||
*.map
|
||||
*.generated.*
|
||||
|
||||
# IDE/editor
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Misc
|
||||
LICENSE
|
||||
.gitignore
|
||||
.editorconfig
|
||||
.prettierrc
|
||||
.eslintrc*
|
||||
*.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Starter File Generator
|
||||
|
||||
New file: `packages/core/src/ignore-generator.ts`
|
||||
|
||||
### API
|
||||
|
||||
```typescript
|
||||
export function generateStarterIgnoreFile(projectRoot: string): string;
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
- Deterministic code — scans the project directory for common patterns
|
||||
- Returns the file content as a string (caller writes it to disk)
|
||||
- All suggestions are **commented out** — user must uncomment to activate
|
||||
- Header comment explains the file, syntax, and built-in defaults
|
||||
|
||||
### Detection Logic
|
||||
|
||||
| If exists | Suggest |
|
||||
|-----------|---------|
|
||||
| `__tests__/` or `*.test.*` files | `# __tests__/`, `# *.test.*`, `# *.spec.*` |
|
||||
| `fixtures/` or `testdata/` | `# fixtures/`, `# testdata/` |
|
||||
| `test/` or `tests/` | `# test/`, `# tests/` |
|
||||
| `.storybook/` | `# .storybook/` |
|
||||
| `docs/` | `# docs/` |
|
||||
| `examples/` | `# examples/` |
|
||||
| `scripts/` | `# scripts/` |
|
||||
| `migrations/` | `# migrations/` |
|
||||
| `*.snap` files | `# *.snap` |
|
||||
| `bin/` (non-.NET, i.e. shell scripts) | `# bin/` |
|
||||
| `obj/` | `# obj/` |
|
||||
|
||||
### Generated File Format
|
||||
|
||||
```
|
||||
# .understandignore — patterns for files/dirs to exclude from analysis
|
||||
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
|
||||
# Lines below are suggestions — uncomment to activate.
|
||||
# Use ! prefix to force-include something excluded by defaults.
|
||||
#
|
||||
# Built-in defaults (always excluded unless negated):
|
||||
# node_modules/, .git/, dist/, build/, bin/, obj/, *.lock, *.min.js, etc.
|
||||
#
|
||||
|
||||
# --- Suggested exclusions (uncomment to activate) ---
|
||||
|
||||
# Test files
|
||||
# __tests__/
|
||||
# *.test.*
|
||||
# *.spec.*
|
||||
|
||||
# Test data
|
||||
# fixtures/
|
||||
# testdata/
|
||||
|
||||
# Documentation
|
||||
# docs/
|
||||
|
||||
# ... (more suggestions based on detection)
|
||||
```
|
||||
|
||||
Only generated if `.understand-anything/.understandignore` doesn't already exist.
|
||||
|
||||
---
|
||||
|
||||
## Skill Integration
|
||||
|
||||
### Phase 0.5: Ignore Setup (new phase in SKILL.md)
|
||||
|
||||
Added between Pre-flight (Phase 0) and SCAN (Phase 1):
|
||||
|
||||
1. Check if `.understand-anything/.understandignore` exists
|
||||
2. If not, run `generateStarterIgnoreFile(projectRoot)` and write the result to `.understand-anything/.understandignore`
|
||||
3. Report to user:
|
||||
- **First run:** "Generated `.understand-anything/.understandignore` with suggested exclusions. Please review it and uncomment any patterns you'd like to exclude. When ready, confirm to continue."
|
||||
- **Subsequent runs:** "Found `.understand-anything/.understandignore`. Review it if needed, then confirm to continue."
|
||||
4. Wait for user confirmation before proceeding
|
||||
|
||||
### Phase 1: SCAN changes
|
||||
|
||||
The `project-scanner` agent's scan script is updated to:
|
||||
|
||||
1. Collect files via `git ls-files` (or fallback)
|
||||
2. Apply agent's hardcoded pattern filter (Layer 1 — existing behavior)
|
||||
3. Apply `IgnoreFilter` from core (Layer 2 — user patterns)
|
||||
4. Add `filteredByIgnore` count to scan output
|
||||
5. Report: "Scanned {totalFiles} files ({filteredByIgnore} excluded by .understandignore)"
|
||||
|
||||
Two-layer filtering:
|
||||
- **Layer 1:** Agent's hardcoded patterns in the prompt (fast, coarse filter)
|
||||
- **Layer 2:** `IgnoreFilter` from core (deterministic code, user-configurable)
|
||||
|
||||
---
|
||||
|
||||
## Project Scanner Agent Update
|
||||
|
||||
Changes to `understand-anything-plugin/agents/project-scanner.md`:
|
||||
|
||||
- After the file list is built and Layer 1 filtering is applied, the agent runs a Node.js script that imports `createIgnoreFilter` from `@understand-anything/core` and filters the remaining paths
|
||||
- The scan result JSON includes a new `filteredByIgnore: number` field
|
||||
- Existing hardcoded exclusion patterns in the agent prompt remain for backward compatibility
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### `packages/core/src/__tests__/ignore-filter.test.ts`
|
||||
|
||||
- Parses basic glob patterns (`*.log`, `dist/`)
|
||||
- Handles `#` comments and blank lines
|
||||
- Handles `!` negation (force-include)
|
||||
- Handles `**/` recursive matching
|
||||
- Handles trailing `/` for directory-only patterns
|
||||
- Merges defaults + user patterns correctly
|
||||
- `!` in user file overrides hardcoded defaults
|
||||
- Returns `false` for paths not matching any pattern
|
||||
|
||||
### `packages/core/src/__tests__/ignore-generator.test.ts`
|
||||
|
||||
- Generates starter file with header comment
|
||||
- Detects existing directories and suggests relevant patterns
|
||||
- All suggestions are commented out (prefixed with `# `)
|
||||
- Doesn't overwrite existing file
|
||||
- Includes bin/obj suggestions when relevant
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `packages/core/src/ignore-filter.ts` | Parse .understandignore, merge with defaults, filter paths |
|
||||
| `packages/core/src/ignore-generator.ts` | Generate starter file by scanning project structure |
|
||||
| `packages/core/src/__tests__/ignore-filter.test.ts` | Filter logic tests |
|
||||
| `packages/core/src/__tests__/ignore-generator.test.ts` | Generator tests |
|
||||
| `agents/project-scanner.md` | Add Layer 2 filtering via IgnoreFilter |
|
||||
| `skills/understand/SKILL.md` | Add Phase 0.5 (generate + pause for review) |
|
||||
| `packages/core/package.json` | Add `ignore` npm dependency |
|
||||
Generated
+9
@@ -42,6 +42,9 @@ importers:
|
||||
fuse.js:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
ignore:
|
||||
specifier: ^7.0.5
|
||||
version: 7.0.5
|
||||
tree-sitter-javascript:
|
||||
specifier: ^0.25.0
|
||||
version: 0.25.0
|
||||
@@ -1586,6 +1589,10 @@ packages:
|
||||
http-cache-semantics@4.2.0:
|
||||
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
|
||||
|
||||
ignore@7.0.5:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
|
||||
@@ -4121,6 +4128,8 @@ snapshots:
|
||||
|
||||
http-cache-semantics@4.2.0: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
|
||||
iron-webcrypto@1.2.1: {}
|
||||
|
||||
@@ -39,7 +39,7 @@ Discover all tracked files. In order of preference:
|
||||
|
||||
Remove ALL files matching these patterns:
|
||||
- **Dependency directories:** paths containing `node_modules/`, `.git/`, `vendor/`, `venv/`, `.venv/`, `__pycache__/`
|
||||
- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust) — match full directory segments only, not substrings (e.g., `buildSrc/` should NOT be excluded)
|
||||
- **Build output:** paths with a directory segment matching `dist/`, `build/`, `out/`, `coverage/`, `.next/`, `.cache/`, `.turbo/`, `target/` (Rust), `obj/` (.NET) — match full directory segments only, not substrings (e.g., `buildSrc/` should NOT be excluded). Note: `bin/` is NOT excluded by default because Node.js and Ruby projects use `bin/` for CLI launchers; .NET users can add `bin/` to `.understandignore`.
|
||||
- **Lock files:** `*.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
|
||||
- **Binary/asset files:** `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.mp3`, `.mp4`, `.pdf`, `.zip`, `.tar`, `.gz`
|
||||
- **Generated files:** `*.min.js`, `*.min.css`, `*.map`, `*.generated.*` (note: do NOT exclude `*.d.ts` — many projects have hand-written declaration files)
|
||||
@@ -58,6 +58,18 @@ Remove ALL files matching these patterns:
|
||||
|
||||
**Note on package manifests:** Config files read for framework detection (`package.json`, `tsconfig.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, etc.) should also appear in the file list with `fileCategory: "config"`.
|
||||
|
||||
**Step 2.5 -- User-Configured Filtering (.understandignore)**
|
||||
|
||||
When `.understandignore` files exist, **replace** Step 2's hardcoded filtering with a unified filter that combines defaults and user patterns in a single pass. This ensures `!` negation patterns can override defaults.
|
||||
|
||||
1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists. If so, read it.
|
||||
2. Check if `$PROJECT_ROOT/.understandignore` exists. If so, read it.
|
||||
3. If neither file exists, skip this step entirely — Step 2's hardcoded filtering is sufficient.
|
||||
4. If at least one file exists, re-filter the **original file list from Step 1** (not the Step 2 output) using the `createIgnoreFilter` function from `@understand-anything/core`, which merges hardcoded defaults and user patterns into a single `.gitignore`-compatible matcher. This ensures `!` negation in user files can override hardcoded defaults (e.g., `!dist/` force-includes dist/ files).
|
||||
5. Track the count of additional files removed beyond Step 2's baseline as `filteredByIgnore`.
|
||||
|
||||
This filtering must be deterministic (not LLM-based). Use a Node.js script with the `ignore` npm package from `@understand-anything/core`.
|
||||
|
||||
**Step 3 -- Language Detection**
|
||||
|
||||
Map file extensions to language identifiers:
|
||||
@@ -217,6 +229,7 @@ The script must write this exact JSON structure to the output file:
|
||||
{"path": "package.json", "language": "json", "sizeLines": 35, "fileCategory": "config"}
|
||||
],
|
||||
"totalFiles": 42,
|
||||
"filteredByIgnore": 0,
|
||||
"estimatedComplexity": "moderate",
|
||||
"importMap": {
|
||||
"src/index.ts": ["src/utils.ts", "src/config.ts"],
|
||||
@@ -237,6 +250,7 @@ The script must write this exact JSON structure to the output file:
|
||||
- `files` (object[]) -- every discovered file, sorted by `path` alphabetically
|
||||
- `files[].fileCategory` (string) -- one of: `code`, `config`, `docs`, `infra`, `data`, `script`, `markup`
|
||||
- `totalFiles` (integer) -- must equal `files.length`
|
||||
- `filteredByIgnore` (integer) -- count of files removed by `.understandignore` patterns in Step 2.5; 0 if no `.understandignore` file exists
|
||||
- `estimatedComplexity` (string) -- one of `small`, `moderate`, `large`, `very-large`
|
||||
- `importMap` (object) -- map from every file path to its list of resolved project-internal import paths; empty array for non-code files and files with no resolved imports; external packages excluded
|
||||
|
||||
@@ -281,6 +295,7 @@ Then assemble the final output JSON:
|
||||
{"path": "Dockerfile", "language": "dockerfile", "sizeLines": 22, "fileCategory": "infra"}
|
||||
],
|
||||
"totalFiles": 42,
|
||||
"filteredByIgnore": 0,
|
||||
"estimatedComplexity": "moderate",
|
||||
"importMap": {
|
||||
"src/index.ts": ["src/utils.ts"]
|
||||
@@ -295,6 +310,7 @@ Then assemble the final output JSON:
|
||||
- `frameworks` (string[]): directly from script output
|
||||
- `files` (object[]): directly from script output, including `fileCategory` per file
|
||||
- `totalFiles` (integer): directly from script output
|
||||
- `filteredByIgnore` (integer): directly from script output
|
||||
- `estimatedComplexity` (string): directly from script output
|
||||
- `importMap` (object): directly from script output
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"fuse.js": "^7.1.0",
|
||||
"ignore": "^7.0.5",
|
||||
"tree-sitter-javascript": "^0.25.0",
|
||||
"tree-sitter-typescript": "^0.23.2",
|
||||
"web-tree-sitter": "^0.26.6",
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createIgnoreFilter, DEFAULT_IGNORE_PATTERNS } from "../ignore-filter";
|
||||
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
describe("IgnoreFilter", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `ignore-filter-test-${Date.now()}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
mkdirSync(join(testDir, ".understand-anything"), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("DEFAULT_IGNORE_PATTERNS", () => {
|
||||
it("contains node_modules", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("node_modules/");
|
||||
});
|
||||
|
||||
it("contains .git", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain(".git/");
|
||||
});
|
||||
|
||||
it("contains obj for .NET", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("obj/");
|
||||
});
|
||||
|
||||
it("does not contain bin (used by Node/Ruby CLI launchers)", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).not.toContain("bin/");
|
||||
});
|
||||
|
||||
it("contains build output directories", () => {
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("dist/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("build/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("out/");
|
||||
expect(DEFAULT_IGNORE_PATTERNS).toContain("coverage/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createIgnoreFilter with no user file", () => {
|
||||
it("ignores files matching default patterns", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("node_modules/foo/bar.js")).toBe(true);
|
||||
expect(filter.isIgnored("dist/index.js")).toBe(true);
|
||||
expect(filter.isIgnored(".git/config")).toBe(true);
|
||||
expect(filter.isIgnored("obj/Release/net8.0/app.dll")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not ignore source files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
expect(filter.isIgnored("README.md")).toBe(false);
|
||||
expect(filter.isIgnored("package.json")).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores lock files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("pnpm-lock.yaml")).toBe(true);
|
||||
expect(filter.isIgnored("package-lock.json")).toBe(true);
|
||||
expect(filter.isIgnored("yarn.lock")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores binary/asset files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("logo.png")).toBe(true);
|
||||
expect(filter.isIgnored("font.woff2")).toBe(true);
|
||||
expect(filter.isIgnored("doc.pdf")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores generated files", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("bundle.min.js")).toBe(true);
|
||||
expect(filter.isIgnored("style.min.css")).toBe(true);
|
||||
expect(filter.isIgnored("source.map")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores IDE directories", () => {
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored(".idea/workspace.xml")).toBe(true);
|
||||
expect(filter.isIgnored(".vscode/settings.json")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createIgnoreFilter with user .understandignore", () => {
|
||||
it("reads patterns from .understand-anything/.understandignore", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"# Exclude tests\n__tests__/\n*.test.ts\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("__tests__/foo.test.ts")).toBe(true);
|
||||
expect(filter.isIgnored("src/utils.test.ts")).toBe(true);
|
||||
expect(filter.isIgnored("src/utils.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("reads patterns from project root .understandignore", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understandignore"),
|
||||
"docs/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("docs/README.md")).toBe(true);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles # comments and blank lines", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"# This is a comment\n\n\nfixtures/\n\n# Another comment\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("fixtures/data.json")).toBe(true);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("supports ! negation to override defaults", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"!dist/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("dist/index.js")).toBe(false);
|
||||
});
|
||||
|
||||
it("supports ** recursive matching", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"**/snapshots/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("src/components/snapshots/Button.snap")).toBe(true);
|
||||
expect(filter.isIgnored("snapshots/foo.snap")).toBe(true);
|
||||
});
|
||||
|
||||
it("merges .understand-anything/ and root .understandignore", () => {
|
||||
writeFileSync(
|
||||
join(testDir, ".understand-anything", ".understandignore"),
|
||||
"__tests__/\n"
|
||||
);
|
||||
writeFileSync(
|
||||
join(testDir, ".understandignore"),
|
||||
"fixtures/\n"
|
||||
);
|
||||
const filter = createIgnoreFilter(testDir);
|
||||
expect(filter.isIgnored("__tests__/foo.ts")).toBe(true);
|
||||
expect(filter.isIgnored("fixtures/data.json")).toBe(true);
|
||||
expect(filter.isIgnored("src/index.ts")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { generateStarterIgnoreFile } from "../ignore-generator";
|
||||
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
describe("generateStarterIgnoreFile", () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = join(tmpdir(), `ignore-gen-test-${Date.now()}`);
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("includes a header comment explaining the file", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain(".understandignore");
|
||||
expect(content).toContain("same as .gitignore");
|
||||
expect(content).toContain("Built-in defaults");
|
||||
});
|
||||
|
||||
it("all suggestions are commented out", () => {
|
||||
mkdirSync(join(testDir, "__tests__"), { recursive: true });
|
||||
mkdirSync(join(testDir, "docs"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
||||
expect(lines).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("suggests __tests__ when directory exists", () => {
|
||||
mkdirSync(join(testDir, "__tests__"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# __tests__/");
|
||||
});
|
||||
|
||||
it("suggests docs when directory exists", () => {
|
||||
mkdirSync(join(testDir, "docs"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# docs/");
|
||||
});
|
||||
|
||||
it("suggests test and tests when they exist", () => {
|
||||
mkdirSync(join(testDir, "test"), { recursive: true });
|
||||
mkdirSync(join(testDir, "tests"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# test/");
|
||||
expect(content).toContain("# tests/");
|
||||
});
|
||||
|
||||
it("suggests fixtures when directory exists", () => {
|
||||
mkdirSync(join(testDir, "fixtures"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# fixtures/");
|
||||
});
|
||||
|
||||
it("suggests examples when directory exists", () => {
|
||||
mkdirSync(join(testDir, "examples"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# examples/");
|
||||
});
|
||||
|
||||
it("suggests .storybook when directory exists", () => {
|
||||
mkdirSync(join(testDir, ".storybook"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# .storybook/");
|
||||
});
|
||||
|
||||
it("suggests migrations when directory exists", () => {
|
||||
mkdirSync(join(testDir, "migrations"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# migrations/");
|
||||
});
|
||||
|
||||
it("suggests scripts when directory exists", () => {
|
||||
mkdirSync(join(testDir, "scripts"), { recursive: true });
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# scripts/");
|
||||
});
|
||||
|
||||
it("always includes generic test file suggestions", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# *.snap");
|
||||
expect(content).toContain("# *.test.*");
|
||||
expect(content).toContain("# *.spec.*");
|
||||
});
|
||||
|
||||
it("does not suggest directories that don't exist", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).not.toContain("# __tests__/");
|
||||
expect(content).not.toContain("# .storybook/");
|
||||
expect(content).not.toContain("# fixtures/");
|
||||
});
|
||||
|
||||
describe(".gitignore integration", () => {
|
||||
it("includes .gitignore patterns not covered by defaults", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("From .gitignore");
|
||||
expect(content).toContain("# .env");
|
||||
expect(content).toContain("# secrets/");
|
||||
expect(content).toContain("# *.pyc");
|
||||
});
|
||||
|
||||
it("excludes .gitignore patterns already in defaults", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), "node_modules/\ndist/\n.env\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
// .env is not in defaults, should appear
|
||||
expect(content).toContain("# .env");
|
||||
// node_modules/ and dist/ are in defaults, should not appear in .gitignore section
|
||||
const gitignoreSection = content.split("From .gitignore")[1]?.split("---")[0] ?? "";
|
||||
expect(gitignoreSection).not.toContain("node_modules");
|
||||
expect(gitignoreSection).not.toContain("dist");
|
||||
});
|
||||
|
||||
it("skips .gitignore comments and blank lines", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), "# a comment\n\n.env\n \n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("# .env");
|
||||
// Should not include the original comment as a pattern
|
||||
const gitignoreSection = content.split("From .gitignore")[1]?.split("---")[0] ?? "";
|
||||
expect(gitignoreSection).not.toContain("a comment");
|
||||
});
|
||||
|
||||
it("handles .gitignore with trailing-slash normalization for defaults", () => {
|
||||
// "dist" without trailing slash should still match "dist/" default
|
||||
writeFileSync(join(testDir, ".gitignore"), "dist\ncoverage\n.env\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).toContain("From .gitignore");
|
||||
// Extract lines between the .gitignore header and the next section header
|
||||
const lines = content.split("\n");
|
||||
const headerIdx = lines.findIndex((l) => l.includes("From .gitignore"));
|
||||
const nextSectionIdx = lines.findIndex((l, i) => i > headerIdx && l.startsWith("# ---"));
|
||||
const sectionLines = lines.slice(headerIdx + 1, nextSectionIdx === -1 ? undefined : nextSectionIdx);
|
||||
const patterns = sectionLines.filter((l) => l.startsWith("# ") && !l.startsWith("# ---")).map((l) => l.slice(2));
|
||||
expect(patterns).toContain(".env");
|
||||
expect(patterns).not.toContain("dist");
|
||||
expect(patterns).not.toContain("coverage");
|
||||
});
|
||||
|
||||
it("omits .gitignore section when no .gitignore exists", () => {
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).not.toContain("From .gitignore");
|
||||
});
|
||||
|
||||
it("omits .gitignore section when all patterns are covered by defaults", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), "node_modules/\ndist/\n*.lock\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
expect(content).not.toContain("From .gitignore");
|
||||
});
|
||||
|
||||
it("all .gitignore suggestions are commented out", () => {
|
||||
writeFileSync(join(testDir, ".gitignore"), ".env\nsecrets/\n*.pyc\n");
|
||||
const content = generateStarterIgnoreFile(testDir);
|
||||
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
||||
expect(lines).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import ignore, { type Ignore } from "ignore";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Hardcoded default ignore patterns matching the project-scanner agent's
|
||||
* exclusion rules, plus bin/obj for .NET projects.
|
||||
*/
|
||||
export const DEFAULT_IGNORE_PATTERNS: string[] = [
|
||||
// Dependency directories
|
||||
"node_modules/",
|
||||
".git/",
|
||||
"vendor/",
|
||||
"venv/",
|
||||
".venv/",
|
||||
"__pycache__/",
|
||||
|
||||
// Build output
|
||||
"dist/",
|
||||
"build/",
|
||||
"out/",
|
||||
"coverage/",
|
||||
".next/",
|
||||
".cache/",
|
||||
".turbo/",
|
||||
"target/",
|
||||
"obj/",
|
||||
|
||||
// Lock files
|
||||
"*.lock",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
|
||||
// Binary/asset files
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.gif",
|
||||
"*.svg",
|
||||
"*.ico",
|
||||
"*.woff",
|
||||
"*.woff2",
|
||||
"*.ttf",
|
||||
"*.eot",
|
||||
"*.mp3",
|
||||
"*.mp4",
|
||||
"*.pdf",
|
||||
"*.zip",
|
||||
"*.tar",
|
||||
"*.gz",
|
||||
|
||||
// Generated files
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
"*.map",
|
||||
"*.generated.*",
|
||||
|
||||
// IDE/editor
|
||||
".idea/",
|
||||
".vscode/",
|
||||
|
||||
// Misc
|
||||
"LICENSE",
|
||||
".gitignore",
|
||||
".editorconfig",
|
||||
".prettierrc",
|
||||
".eslintrc*",
|
||||
"*.log",
|
||||
];
|
||||
|
||||
export interface IgnoreFilter {
|
||||
/** Returns true if the given relative path should be excluded from analysis. */
|
||||
isIgnored(relativePath: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an IgnoreFilter that merges hardcoded defaults with user-defined
|
||||
* patterns from .understandignore files.
|
||||
*
|
||||
* Pattern load order (later entries can override earlier ones via ! negation):
|
||||
* 1. Hardcoded defaults
|
||||
* 2. .understand-anything/.understandignore (if exists)
|
||||
* 3. .understandignore at project root (if exists)
|
||||
*/
|
||||
export function createIgnoreFilter(projectRoot: string): IgnoreFilter {
|
||||
const ig: Ignore = ignore();
|
||||
|
||||
// Layer 1: hardcoded defaults
|
||||
ig.add(DEFAULT_IGNORE_PATTERNS);
|
||||
|
||||
// Layer 2: .understand-anything/.understandignore
|
||||
const projectIgnorePath = join(projectRoot, ".understand-anything", ".understandignore");
|
||||
if (existsSync(projectIgnorePath)) {
|
||||
const content = readFileSync(projectIgnorePath, "utf-8");
|
||||
ig.add(content);
|
||||
}
|
||||
|
||||
// Layer 3: .understandignore at project root
|
||||
const rootIgnorePath = join(projectRoot, ".understandignore");
|
||||
if (existsSync(rootIgnorePath)) {
|
||||
const content = readFileSync(rootIgnorePath, "utf-8");
|
||||
ig.add(content);
|
||||
}
|
||||
|
||||
return {
|
||||
isIgnored(relativePath: string): boolean {
|
||||
return ig.ignores(relativePath);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_IGNORE_PATTERNS } from "./ignore-filter.js";
|
||||
|
||||
const HEADER = `# .understandignore — patterns for files/dirs to exclude from analysis
|
||||
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
|
||||
# Lines below are suggestions — uncomment to activate.
|
||||
# Use ! prefix to force-include something excluded by defaults.
|
||||
#
|
||||
# Built-in defaults (always excluded unless negated):
|
||||
# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.
|
||||
#
|
||||
`;
|
||||
|
||||
const DETECTABLE_DIRS = [
|
||||
{ dir: "__tests__", pattern: "__tests__/" },
|
||||
{ dir: "test", pattern: "test/" },
|
||||
{ dir: "tests", pattern: "tests/" },
|
||||
{ dir: "fixtures", pattern: "fixtures/" },
|
||||
{ dir: "testdata", pattern: "testdata/" },
|
||||
{ dir: "docs", pattern: "docs/" },
|
||||
{ dir: "examples", pattern: "examples/" },
|
||||
{ dir: "scripts", pattern: "scripts/" },
|
||||
{ dir: "migrations", pattern: "migrations/" },
|
||||
{ dir: ".storybook", pattern: ".storybook/" },
|
||||
];
|
||||
|
||||
const GENERIC_SUGGESTIONS = [
|
||||
"*.test.*",
|
||||
"*.spec.*",
|
||||
"*.snap",
|
||||
];
|
||||
|
||||
/**
|
||||
* Parses a .gitignore file and returns active patterns (no comments, no blanks).
|
||||
*/
|
||||
function parseGitignorePatterns(gitignorePath: string): string[] {
|
||||
if (!existsSync(gitignorePath)) return [];
|
||||
const content = readFileSync(gitignorePath, "utf-8");
|
||||
return content
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a gitignore pattern is already covered by the hardcoded defaults.
|
||||
* Normalizes trailing slashes for comparison.
|
||||
*/
|
||||
function isCoveredByDefaults(pattern: string): boolean {
|
||||
const normalize = (p: string) => p.replace(/\/+$/, "");
|
||||
const normalized = normalize(pattern);
|
||||
return DEFAULT_IGNORE_PATTERNS.some((d) => normalize(d) === normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a starter .understandignore file content by scanning the project
|
||||
* for common directories and reading .gitignore patterns.
|
||||
* All suggestions are commented out — this is a one-time generation.
|
||||
*/
|
||||
export function generateStarterIgnoreFile(projectRoot: string): string {
|
||||
const sections: string[] = [HEADER];
|
||||
|
||||
// Section 1: patterns from .gitignore not already in defaults
|
||||
const gitignorePath = join(projectRoot, ".gitignore");
|
||||
const gitignorePatterns = parseGitignorePatterns(gitignorePath).filter(
|
||||
(p) => !isCoveredByDefaults(p),
|
||||
);
|
||||
|
||||
if (gitignorePatterns.length > 0) {
|
||||
sections.push("# --- From .gitignore (uncomment to exclude) ---\n");
|
||||
for (const pattern of gitignorePatterns) {
|
||||
sections.push(`# ${pattern}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Section 2: detected directories
|
||||
const detected: string[] = [];
|
||||
for (const { dir, pattern } of DETECTABLE_DIRS) {
|
||||
if (existsSync(join(projectRoot, dir))) {
|
||||
detected.push(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
if (detected.length > 0) {
|
||||
sections.push("# --- Detected directories (uncomment to exclude) ---\n");
|
||||
for (const pattern of detected) {
|
||||
sections.push(`# ${pattern}`);
|
||||
}
|
||||
sections.push("");
|
||||
}
|
||||
|
||||
// Section 3: generic test patterns
|
||||
sections.push("# --- Test file patterns (uncomment to exclude) ---\n");
|
||||
for (const pattern of GENERIC_SUGGESTIONS) {
|
||||
sections.push(`# ${pattern}`);
|
||||
}
|
||||
sections.push("");
|
||||
|
||||
return sections.join("\n");
|
||||
}
|
||||
@@ -114,3 +114,9 @@ export {
|
||||
ShellParser,
|
||||
registerAllParsers,
|
||||
} from "./plugins/parsers/index.js";
|
||||
export {
|
||||
createIgnoreFilter,
|
||||
DEFAULT_IGNORE_PATTERNS,
|
||||
type IgnoreFilter,
|
||||
} from "./ignore-filter.js";
|
||||
export { generateStarterIgnoreFile } from "./ignore-generator.js";
|
||||
|
||||
+18
-1
@@ -27,6 +27,9 @@ importers:
|
||||
fuse.js:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
ignore:
|
||||
specifier: ^7.0.5
|
||||
version: 7.0.5
|
||||
tree-sitter-javascript:
|
||||
specifier: ^0.25.0
|
||||
version: 0.25.0
|
||||
@@ -1015,6 +1018,10 @@ packages:
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
ignore@7.0.5:
|
||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
|
||||
@@ -2203,6 +2210,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
@@ -2513,6 +2528,8 @@ snapshots:
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
@@ -3283,7 +3300,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
|
||||
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
|
||||
@@ -77,6 +77,47 @@ Determine whether to run a full analysis or incremental update.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0.5 — Ignore Configuration
|
||||
|
||||
Set up and verify the `.understandignore` file before scanning.
|
||||
|
||||
1. Check if `$PROJECT_ROOT/.understand-anything/.understandignore` exists.
|
||||
2. **If it does NOT exist**, generate a starter file:
|
||||
- Run the following Node.js one-liner in `$PROJECT_ROOT` (reads `.gitignore` and deduplicates against built-in defaults):
|
||||
```bash
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const root = process.cwd();
|
||||
const defaults = ['node_modules/','node_modules','.git/','vendor/','venv/','.venv/','__pycache__/','dist/','dist','build/','build','out/','coverage/','coverage','.next/','.cache/','.turbo/','target/','obj/','*.lock','package-lock.json','yarn.lock','pnpm-lock.yaml','*.png','*.jpg','*.jpeg','*.gif','*.svg','*.ico','*.woff','*.woff2','*.ttf','*.eot','*.mp3','*.mp4','*.pdf','*.zip','*.tar','*.gz','*.min.js','*.min.css','*.map','*.generated.*','.idea/','.vscode/','LICENSE','.gitignore','.editorconfig','.prettierrc','.eslintrc*','*.log'];
|
||||
const norm = p => p.replace(/\/+$/, '');
|
||||
const defaultSet = new Set(defaults.map(norm));
|
||||
const header = '# .understandignore — patterns for files/dirs to exclude from analysis\n# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)\n# Lines below are suggestions — uncomment to activate.\n# Use ! prefix to force-include something excluded by defaults.\n#\n# Built-in defaults (always excluded unless negated):\n# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.\n#\n';
|
||||
let body = '';
|
||||
const gitignorePath = path.join(root, '.gitignore');
|
||||
if (fs.existsSync(gitignorePath)) {
|
||||
const gi = fs.readFileSync(gitignorePath, 'utf-8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).filter(p => !defaultSet.has(norm(p)));
|
||||
if (gi.length) { body += '# --- From .gitignore (uncomment to exclude) ---\n\n' + gi.map(p => '# ' + p).join('\n') + '\n\n'; }
|
||||
}
|
||||
const dirs = ['__tests__','test','tests','fixtures','testdata','docs','examples','scripts','migrations','.storybook'];
|
||||
const found = dirs.filter(d => fs.existsSync(path.join(root, d)));
|
||||
if (found.length) { body += '# --- Detected directories (uncomment to exclude) ---\n\n' + found.map(d => '# ' + d + '/').join('\n') + '\n\n'; }
|
||||
body += '# --- Test file patterns (uncomment to exclude) ---\n\n# *.test.*\n# *.spec.*\n# *.snap\n';
|
||||
const outDir = path.join(root, '.understand-anything');
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(outDir, '.understandignore'), header + body);
|
||||
"
|
||||
```
|
||||
- Report to the user:
|
||||
> Generated `.understand-anything/.understandignore` with suggested exclusions based on your project structure. Please review it and uncomment any patterns you'd like to exclude from analysis. When ready, confirm to continue.
|
||||
- **Wait for user confirmation before proceeding.**
|
||||
3. **If it already exists**, report:
|
||||
> Found `.understand-anything/.understandignore`. Review it if needed, then confirm to continue.
|
||||
- **Wait for user confirmation before proceeding.**
|
||||
4. After confirmation, proceed to Phase 1.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — SCAN (Full analysis only)
|
||||
|
||||
Dispatch a subagent using the `project-scanner` agent definition (at `agents/project-scanner.md`). Append the following additional context:
|
||||
@@ -113,6 +154,9 @@ Store the file list as `$FILE_LIST` with `fileCategory` metadata for use in Phas
|
||||
|
||||
**Gate check:** If >100 files, inform the user and suggest scoping with a subdirectory argument. Proceed only if user confirms or add guidance that this may take a while.
|
||||
|
||||
If the scan result includes `filteredByIgnore > 0`, report:
|
||||
> Excluded {filteredByIgnore} files via `.understandignore`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — ANALYZE
|
||||
|
||||
Reference in New Issue
Block a user