merge: integrate latest main into benchmark branch
This commit is contained in:
@@ -532,6 +532,27 @@ describe('scan-project.mjs — data-dir resolution (.ua vs legacy)', () => {
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.output.files.map(file => file.path)).toEqual(['src/index.ts']);
|
||||
});
|
||||
|
||||
it('composes CLI exclusions for benchmark scans regardless of flag order', () => {
|
||||
projectRoot = setupTree({
|
||||
'.ua/knowledge-graph.json': '{ "nodes": [] }\n',
|
||||
'.understand-anything/meta.json': '{ "version": 1 }\n',
|
||||
'generated/client.ts': 'export const generated = true;\n',
|
||||
'src/index.ts': 'export const x = 1;\n',
|
||||
});
|
||||
|
||||
for (const args of [
|
||||
['--exclude', 'generated/', '--exclude-analysis-data'],
|
||||
['--exclude-analysis-data', '--exclude', 'generated/'],
|
||||
]) {
|
||||
const r = runScript(projectRoot, args);
|
||||
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.output.files.map(file => file.path)).toEqual(['src/index.ts']);
|
||||
expect(r.output.filteredByIgnore).toBe(1);
|
||||
expect(r.output.contentDigest).toMatch(/^[0-9a-f]{64}$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('scan-project.mjs — special-file recognition', () => {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
---
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -261,7 +271,7 @@ 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`.
|
||||
> Excluded {filteredByIgnore} files via `.understandignore` and/or `--exclude` rules.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
*
|
||||
* What this script owns:
|
||||
* - File enumeration (git ls-files preferred, recursive walk fallback)
|
||||
* - `.understandignore` filtering (delegated to core's createIgnoreFilter,
|
||||
* which reads the resolved data dir — `.ua/`, or legacy
|
||||
* `.understand-anything/` when that directory already exists)
|
||||
* - `.understandignore` and CLI exclusion filtering (delegated to core's
|
||||
* createIgnoreFilter, which reads the resolved data dir — `.ua/`, or
|
||||
* legacy `.understand-anything/` when that directory already exists)
|
||||
* - Per-file language detection (extension + filename table)
|
||||
* - Per-file category assignment (priority-ordered rules from
|
||||
* project-scanner.md Step 4)
|
||||
@@ -26,7 +26,13 @@
|
||||
* - Complexity estimation (project-scanner.md Step 7 thresholds)
|
||||
*
|
||||
* Usage:
|
||||
* node scan-project.mjs <projectRoot> <outputPath> [--exclude-analysis-data]
|
||||
* node scan-project.mjs <projectRoot> <outputPath>
|
||||
* [--exclude <patterns>] [--exclude-analysis-data]
|
||||
*
|
||||
* --exclude <patterns> Comma-separated gitignore-style patterns to
|
||||
* additionally exclude from the scan.
|
||||
* --exclude-analysis-data Always exclude persistent `.ua/` and legacy
|
||||
* `.understand-anything/` analysis data.
|
||||
*
|
||||
* 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
|
||||
@@ -582,9 +588,9 @@ function enumerateFiles(projectRoot) {
|
||||
// Filter accounting
|
||||
//
|
||||
// The project-scanner.md contract requires `filteredByIgnore` to count files
|
||||
// dropped *specifically* by user `.understandignore` patterns (the delta
|
||||
// beyond what the hardcoded defaults would have removed). We accomplish this
|
||||
// by building TWO filters:
|
||||
// dropped specifically by user `.understandignore` or CLI `--exclude`
|
||||
// patterns (the delta beyond what the hardcoded defaults would have removed).
|
||||
// We accomplish this by building TWO filters:
|
||||
// - `defaultOnly`: defaults only, no user patterns
|
||||
// - `combined`: defaults + user patterns (createIgnoreFilter)
|
||||
// and counting paths that the combined filter excludes but the defaults-only
|
||||
@@ -686,20 +692,56 @@ function updateContentDigest(hash, posixPath, contentBytes) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
const [, , projectRoot, outputPath, ...options] = process.argv;
|
||||
const args = process.argv.slice(2);
|
||||
let projectRoot;
|
||||
let outputPath;
|
||||
let excludeAnalysisData = false;
|
||||
const excludePatterns = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === '--exclude-analysis-data') {
|
||||
excludeAnalysisData = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--exclude') {
|
||||
const value = args[i + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
process.stderr.write('scan-project.mjs failed: --exclude requires patterns\n');
|
||||
process.exit(1);
|
||||
}
|
||||
excludePatterns.push(
|
||||
...value
|
||||
.split(',')
|
||||
.map(pattern => pattern.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith('--')) {
|
||||
process.stderr.write(`scan-project.mjs failed: unknown option: ${arg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!projectRoot) {
|
||||
projectRoot = arg;
|
||||
continue;
|
||||
}
|
||||
if (!outputPath) {
|
||||
outputPath = arg;
|
||||
continue;
|
||||
}
|
||||
process.stderr.write(`scan-project.mjs failed: unexpected argument: ${arg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!projectRoot || !outputPath) {
|
||||
process.stderr.write(
|
||||
'Usage: node scan-project.mjs <projectRoot> <outputPath> ' +
|
||||
'[--exclude-analysis-data]\n',
|
||||
'[--exclude <patterns>] [--exclude-analysis-data]\n',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const unknownOption = options.find(option => option !== '--exclude-analysis-data');
|
||||
if (unknownOption) {
|
||||
process.stderr.write(`scan-project.mjs failed: unknown option: ${unknownOption}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const excludeAnalysisData = options.includes('--exclude-analysis-data');
|
||||
|
||||
if (!existsSync(projectRoot)) {
|
||||
process.stderr.write(
|
||||
@@ -722,10 +764,10 @@ async function main() {
|
||||
(!rel.startsWith('.ua/') && !rel.startsWith('.understand-anything/')),
|
||||
);
|
||||
|
||||
// 2. Filter via createIgnoreFilter (defaults + user .understandignore).
|
||||
// 2. Filter via createIgnoreFilter (defaults + .understandignore + CLI excludes).
|
||||
// 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;
|
||||
@@ -739,7 +781,7 @@ async function main() {
|
||||
// Dropped by combined filter. If defaults-only would have ALSO dropped
|
||||
// it, this is a baseline default drop — not counted. If defaults-only
|
||||
// would have KEPT it, this drop is attributable to the user's
|
||||
// .understandignore content.
|
||||
// .understandignore content or CLI --exclude patterns.
|
||||
if (userIgnoresPresent && !defaultsOnly.isIgnored(rel)) {
|
||||
filteredByIgnore++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user