Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions understand-anything-plugin/agents/project-scanner.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ If the manifest is missing or malformed, leave the corresponding field empty rat

Invoke the bundled scan script. It walks the project (preferring `git ls-files`, falling back to a recursive walk for non-git directories), applies `.understandignore` filtering (defaults + user patterns), assigns `language` and `fileCategory` per the canonical tables, counts lines, and writes deterministic JSON. You do not see or maintain those tables — they live in the script.

If the dispatch prompt includes exclude patterns, append `--exclude "<patterns>"` to the invocation (patterns should be comma-separated; the script splits them internally).

Resolve the project's data directory once (the legacy `.understand-anything/` when it already exists, otherwise the new `.ua/`) and reuse `$UA_DIR` for every path below:

```bash
Expand All @@ -64,6 +66,15 @@ node $PLUGIN_ROOT/skills/understand/scan-project.mjs \
"$UA_DIR/tmp/ua-scan-files.json"
```

With exclude patterns (add the `--exclude` flag after the output path):

```bash
node $PLUGIN_ROOT/skills/understand/scan-project.mjs \
"$PROJECT_ROOT" \
"$UA_DIR/tmp/ua-scan-files.json" \
--exclude "tests/*,docs/*"
```

Output JSON shape (you will read this verbatim and merge into the final scan-result):

```json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,56 @@ describe("IgnoreFilter", () => {
expect(filter.isIgnored("src/index.ts")).toBe(false);
});
});

describe("createIgnoreFilter with CLI --exclude patterns", () => {
it("applies CLI exclude patterns alongside defaults", () => {
const filter = createIgnoreFilter(testDir, ["tests/", "e2e/"]);
expect(filter.isIgnored("tests/foo.test.ts")).toBe(true);
expect(filter.isIgnored("e2e/smoke.spec.ts")).toBe(true);
// Defaults still apply
expect(filter.isIgnored("node_modules/foo.js")).toBe(true);
expect(filter.isIgnored("dist/bundle.js")).toBe(true);
// Non-excluded source files pass through
expect(filter.isIgnored("src/index.ts")).toBe(false);
expect(filter.isIgnored("README.md")).toBe(false);
});

it("CLI patterns have highest priority over .understandignore files", () => {
// .understandignore says to include docs/
writeFileSync(
join(testDir, ".understand-anything", ".understandignore"),
"!docs/\n"
);
// CLI --exclude says to exclude docs/
const filter = createIgnoreFilter(testDir, ["docs/"]);
// CLI patterns are added last, so they override the ! negation from .understandignore
expect(filter.isIgnored("docs/README.md")).toBe(true);
});

it("CLI ! negation can re-include files excluded by defaults", () => {
// CLI says to include dist/ even though defaults exclude it
const filter = createIgnoreFilter(testDir, ["!dist/"]);
expect(filter.isIgnored("dist/bundle.js")).toBe(false);
// Other defaults still apply
expect(filter.isIgnored("node_modules/foo.js")).toBe(true);
});

it("CLI patterns combined with .understandignore files all apply", () => {
writeFileSync(
join(testDir, ".understandignore"),
"fixtures/\n"
);
const filter = createIgnoreFilter(testDir, ["e2e/"]);
expect(filter.isIgnored("fixtures/data.json")).toBe(true);
expect(filter.isIgnored("e2e/smoke.spec.ts")).toBe(true);
expect(filter.isIgnored("src/index.ts")).toBe(false);
});

it("empty CLI patterns array has no effect", () => {
const filter = createIgnoreFilter(testDir, []);
expect(filter.isIgnored("node_modules/foo.js")).toBe(true);
expect(filter.isIgnored("src/index.ts")).toBe(false);
expect(filter.isIgnored("docs/README.md")).toBe(false);
});
});
});
10 changes: 8 additions & 2 deletions understand-anything-plugin/packages/core/src/ignore-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,16 @@ export interface IgnoreFilter {

/**
* Creates an IgnoreFilter that merges hardcoded defaults with user-defined
* patterns from .understandignore files.
* patterns from .understandignore files and CLI-provided exclude patterns.
*
* Pattern load order (later entries can override earlier ones via ! negation):
* 1. Hardcoded defaults
* 2. <ua-dir>/.understandignore (if exists — `.ua/`, or the legacy
* `.understand-anything/` when that directory already exists)
* 3. .understandignore at project root (if exists)
* 4. CLI --exclude patterns (highest priority)
*/
export function createIgnoreFilter(projectRoot: string): IgnoreFilter {
export function createIgnoreFilter(projectRoot: string, extraPatterns: string[] = []): IgnoreFilter {
const ig: Ignore = ignore();

// Layer 1: hardcoded defaults
Expand All @@ -105,6 +106,11 @@ export function createIgnoreFilter(projectRoot: string): IgnoreFilter {
ig.add(content);
}

// Layer 4: CLI --exclude patterns (highest priority)
if (extraPatterns.length > 0) {
ig.add(extraPatterns);
}

return {
isIgnored(relativePath: string): boolean {
return ig.ignores(relativePath);
Expand Down
14 changes: 12 additions & 2 deletions understand-anything-plugin/skills/understand/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: understand
description: Analyze a codebase to produce an interactive knowledge graph for understanding architecture, components, and relationships
argument-hint: "[path] [--full|--auto-update|--no-auto-update|--review|--language <lang>]"
argument-hint: ["[path] [--full|--auto-update|--no-auto-update|--review|--language <lang>|--exclude <patterns>]"]
---

# /understand
Expand All @@ -16,6 +16,7 @@ Analyze the current codebase and produce a `knowledge-graph.json` file in the pr
- `--no-auto-update` — Disable automatic graph updates (writes `autoUpdate: false` to `$UA_DIR/config.json`)
- `--review` — Run full LLM graph-reviewer instead of inline deterministic validation
- `--language <lang>` — Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in the specified language. Accepts ISO 639-1 codes (`zh`, `ja`, `ko`, `en`, `es`, `fr`, `de`, etc.) or friendly names (`chinese`, `japanese`, `korean`, `english`, `spanish`, etc.). Locale variants supported: `zh-TW`, `zh-HK`, etc. Defaults to `en` (English). Stores preference in `$UA_DIR/config.json` for consistency across incremental updates.
- `--exclude <patterns>` — Comma-separated glob patterns for additional files/directories to exclude from analysis (e.g., `--exclude "tests/*,docs/*"`). These patterns take highest priority over built-in defaults and `.understandignore` rules. Supports gitignore syntax including `!` negation.
- A directory path (e.g. `/path/to/repo` or `../other-project`) — Analyze the given directory instead of the current working directory

---
Expand Down Expand Up @@ -162,7 +163,14 @@ Determine whether to run a full analysis or incremental update.
> **Language directive**: Generate all textual content (summaries, descriptions, tags, titles, languageNotes, languageLesson) in **{language}**. Maintain technical accuracy while using natural, native-level phrasing in the target language. Keep technical terms in English when no standard translation exists (e.g., "middleware", "hook", "barrel").
```

4. **Check for subdomain knowledge graphs to merge:**
3.7. **Exclude patterns:**
- Parse `$ARGUMENTS` for `--exclude <patterns>` flag. If found, extract the comma-separated patterns string.
- Split on commas, trim whitespace from each pattern, and filter out empty entries.
- Store the patterns as `$EXCLUDE_PATTERNS` (comma-joined for passing to downstream scripts: `"tests/*,docs/*"`).
- These patterns take highest priority — they are applied on top of default patterns and `.understandignore` rules. Use `!` prefix to force-include files that would otherwise be excluded.
- **Note:** Newly added `--exclude` patterns require a `--full` scan to take effect.

4. **Check for subdomain knowledge graphs to merge:**
List all `*knowledge-graph*.json` files in `$UA_DIR/` **excluding** `knowledge-graph.json` itself (e.g. `frontend-knowledge-graph.json`, `backend-knowledge-graph.json`). If any subdomain graphs exist, run the merge script bundled with this skill (located next to this SKILL.md file — use the skill directory path, not the project root):
```bash
python "<SKILL_DIR>/merge-subdomain-graphs.py" "$PROJECT_ROOT"
Expand Down Expand Up @@ -247,6 +255,8 @@ Pass these parameters in the dispatch prompt:
> Scan this project directory to discover all project files (including non-code files like configs, docs, infrastructure), detect languages and frameworks.
> Project root: `$PROJECT_ROOT`
> Write output to: `$UA_DIR/intermediate/scan-result.json`
>
> Exclude patterns (from --exclude CLI flag; pass to scan-project.mjs via --exclude): $EXCLUDE_PATTERNS

After the subagent completes, read `$UA_DIR/intermediate/scan-result.json` to get:
- Project name, description
Expand Down
31 changes: 25 additions & 6 deletions understand-anything-plugin/skills/understand/scan-project.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
* - Complexity estimation (project-scanner.md Step 7 thresholds)
*
* Usage:
* node scan-project.mjs <projectRoot> <outputPath>
* node scan-project.mjs <projectRoot> <outputPath> [--exclude <patterns>]
*
* --exclude <patterns> Comma-separated glob patterns to additionally exclude.
* These take highest priority over built-in defaults and
* .understandignore rules. Supports gitignore syntax.
*
* Output JSON (subset of what project-scanner.md Phase 1 expects — the LLM
* agent merges this with Step A's narrative fields and Step C's importMap to
Expand Down Expand Up @@ -653,10 +657,25 @@ function countLines(absPath, posixPath) {
// ---------------------------------------------------------------------------

async function main() {
const [, , projectRoot, outputPath] = process.argv;
// Parse CLI arguments: <projectRoot> <outputPath> [--exclude <patterns>]
let projectRoot, outputPath, excludePatterns = [];
for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg === '--exclude' && i + 1 < process.argv.length) {
excludePatterns = process.argv[++i]
.split(',')
.map(p => p.trim())
.filter(Boolean);
} else if (!projectRoot) {
projectRoot = arg;
} else if (!outputPath) {
outputPath = arg;
}
}

if (!projectRoot || !outputPath) {
process.stderr.write(
'Usage: node scan-project.mjs <projectRoot> <outputPath>\n',
'Usage: node scan-project.mjs <projectRoot> <outputPath> [--exclude <patterns>]\n',
);
process.exit(1);
}
Expand All @@ -678,10 +697,10 @@ async function main() {
// 1. Enumerate. Either git ls-files or recursive walk.
const candidates = enumerateFiles(projectRoot);

// 2. Filter via createIgnoreFilter (defaults + user .understandignore).
// 2. Filter via createIgnoreFilter (defaults + user .understandignore + CLI --exclude).
// Build a defaults-only filter in parallel to count user-driven drops.
const combined = createIgnoreFilter(projectRoot);
const userIgnoresPresent = hasUserIgnoreFile(projectRoot);
const combined = createIgnoreFilter(projectRoot, excludePatterns);
const userIgnoresPresent = hasUserIgnoreFile(projectRoot) || excludePatterns.length > 0;
const defaultsOnly = userIgnoresPresent ? buildDefaultsOnlyFilter() : combined;

let filteredByIgnore = 0;
Expand Down