From f1f99fb7fc8c6f47d1d9aaf0fd4d115f44d2e9ee Mon Sep 17 00:00:00 2001 From: drogbaqu Date: Thu, 21 May 2026 19:40:44 +0800 Subject: [PATCH] feat(push): expand default ignore list and exempt features/ The default IGNORED_REF_DIRS only excluded .git/node_modules/coverage/dist/tmp/states/.agents, which left common build-artifact directories on the scan path. On large repos this could push the JSON payload past V8's max string length and produce "Invalid string length" errors during `acai push --all`. Changes: - Extend IGNORED_REF_DIRS with common build/cache dirs: target, build, out, bin, obj, .next, .nuxt, .svelte-kit, .turbo, .cache, .parcel-cache, .vite, .astro, DerivedData, Pods, __pycache__, .venv, venv, .tox, .mypy_cache, .pytest_cache, .ruff_cache, vendor, .idea, .vscode, .DS_Store. - Intentionally do NOT add 'src-tauri'. It contains real Rust source under src-tauri/src/** that should be scanned. The heavy artifact is src-tauri/target/, which is already covered by the 'target' entry. - This set still uses exact-name matching. Glob patterns like '*.egg-info' are intentionally NOT supported here; a separate change introduces .acaiignore for that. - Always descend into the canonical features/ tree even if a subdirectory's name collides with an ignored build dir (e.g. features/build/login.feature.yaml for product 'build'). Tests: 4 new cases in push.test.ts cover target/build/.next/vendor/ __pycache__ exclusion, src-tauri/src descent, and features/build descent. Co-Authored-By: Claude --- src/core/push.test.ts | 77 +++++++++++++++++++++++++++++++++++++++++++ src/core/push.ts | 47 +++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/core/push.test.ts b/src/core/push.test.ts index 2321358..fc2e47d 100644 --- a/src/core/push.test.ts +++ b/src/core/push.test.ts @@ -777,6 +777,83 @@ describe("push planning", () => { "alpha", ]); }); + + test("scanPushRepo skips extended build-output directories like target/, build/, .next/", async () => { + const root = await createRepoFixture({ + "features/alpha.feature.yaml": `feature:\n name: alpha\n product: product-a\ncomponents:\n MAIN:\n requirements:\n 1: Alpha requirement\n`, + "src/main.ts": `// alpha.MAIN.1\n`, + // These should all be skipped during ref scanning. + "target/release/foo.rs": `// alpha.MAIN.1 leak from target\n`, + "build/output.js": `// alpha.MAIN.1 leak from build\n`, + ".next/cache/x.js": `// alpha.MAIN.1 leak from .next\n`, + "vendor/pkg/lib.go": `// alpha.MAIN.1 leak from vendor\n`, + "__pycache__/m.pyc": `// alpha.MAIN.1 leak from __pycache__\n`, + }); + + const runner = createGitRunner({ + "rev-parse --show-toplevel": root, + "log -1 --format=%H -- features/alpha.feature.yaml": "a1", + }); + + const scan = await scanPushRepo({ + cwd: root, + runner: runner as never, + }); + + // Only the legitimate src/main.ts reference should be discovered. + expect(scan.references.map((entry) => entry.path)).toEqual([ + "src/main.ts:1", + ]); + }); + + test("scanPushRepo descends into src-tauri/src so Rust source ACIDs are discovered, while src-tauri/target is still skipped via the target rule", async () => { + const root = await createRepoFixture({ + "features/tauri.feature.yaml": `feature:\n name: tauri\n product: app\ncomponents:\n RUST:\n requirements:\n 1: Native handler\n`, + // Real Rust source — should be scanned. + "src-tauri/src/main.rs": `// tauri.RUST.1\n`, + // Cargo build artifacts — must be skipped. + "src-tauri/target/release/app": `// tauri.RUST.1 leak\n`, + }); + + const runner = createGitRunner({ + "rev-parse --show-toplevel": root, + "log -1 --format=%H -- features/tauri.feature.yaml": "t1", + }); + + const scan = await scanPushRepo({ + cwd: root, + runner: runner as never, + }); + + expect(scan.references.map((entry) => entry.path)).toEqual([ + "src-tauri/src/main.rs:1", + ]); + }); + + test("scanPushRepo descends into features// even when matches a build-output directory (e.g. features/build/)", async () => { + const root = await createRepoFixture({ + // Spec located under features/build/ — should be discovered despite + // "build" being in IGNORED_REF_DIRS. + "features/build/login.feature.yaml": `feature:\n name: login\n product: build\ncomponents:\n FORM:\n requirements:\n 1: Login form\n`, + "features/runtime/checkout.feature.yaml": `feature:\n name: checkout\n product: runtime\ncomponents:\n CART:\n requirements:\n 1: Cart flow\n`, + }); + + const runner = createGitRunner({ + "rev-parse --show-toplevel": root, + "log -1 --format=%H -- features/build/login.feature.yaml": "b1", + "log -1 --format=%H -- features/runtime/checkout.feature.yaml": "r1", + }); + + const scan = await scanPushRepo({ + cwd: root, + runner: runner as never, + }); + + expect(scan.specs.map((entry) => entry.featureName).sort()).toEqual([ + "checkout", + "login", + ]); + }); }); describe("push option normalization", () => { diff --git a/src/core/push.ts b/src/core/push.ts index e9930e0..76a61f5 100644 --- a/src/core/push.ts +++ b/src/core/push.ts @@ -104,6 +104,7 @@ const FEATURE_SPEC_SUFFIX = ".feature.yaml"; const FEATURE_SPEC_PREFIX = "features/"; const UNSCOPED_REFS_BUCKET = ""; const IGNORED_REF_DIRS = new Set([ + // Existing entries ".git", "node_modules", "coverage", @@ -111,6 +112,41 @@ const IGNORED_REF_DIRS = new Set([ "tmp", ".agents", "states", + // Build outputs (added: prevent V8 string-length overflow on large repos) + "target", // Rust/Cargo, Java/Maven, Scala/sbt + "build", // CMake, Gradle, generic + "out", // Next.js export, generic + "bin", // .NET, generic + "obj", // .NET + ".next", // Next.js + ".nuxt", // Nuxt + ".svelte-kit", // SvelteKit + ".turbo", // Turborepo + ".cache", // Many tools + ".parcel-cache", // Parcel + ".vite", // Vite + ".astro", // Astro + "DerivedData", // Xcode + "Pods", // iOS / CocoaPods + // Note: do NOT add `src-tauri` here. It contains real Rust source under + // `src-tauri/src/**` that should be scanned. The heavy artifact is + // `src-tauri/target/`, which is already excluded by the `target` entry above. + // Python + "__pycache__", + ".venv", + "venv", + ".tox", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + // Note: this set uses exact-name matching. Glob patterns like `*.egg-info` + // must be expressed via `.acaiignore` (not yet supported in this PR). + // Go / PHP + "vendor", + // IDE / OS junk + ".idea", + ".vscode", + ".DS_Store", ]); const TEST_PATH_SEGMENTS = new Set(["test", "tests", "__tests__"]); const REF_SCAN_EXCLUDED_SUFFIXES = new Set([ @@ -781,7 +817,16 @@ async function walkFiles(root: string, directory: string): Promise { for (const entry of dirEntries) { if (entry.isDirectory()) { - if (IGNORED_REF_DIRS.has(entry.name)) continue; + // Always descend into the canonical features/ tree, even if a + // subdirectory shares a name with a build-output directory + // (e.g. `features/build/*.feature.yaml` for a product named "build"). + const relativePath = relative(root, join(current, entry.name)) + .split("\\") + .join("/"); + const isUnderFeaturesRoot = + relativePath === FEATURE_SPEC_PREFIX.replace(/\/$/u, "") || + relativePath.startsWith(FEATURE_SPEC_PREFIX); + if (!isUnderFeaturesRoot && IGNORED_REF_DIRS.has(entry.name)) continue; queue.push(join(current, entry.name)); continue; }