import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const abs = (file) => path.join(root, file); const read = (file) => fs.readFileSync(abs(file), "utf8"); const json = (file) => JSON.parse(read(file)); const arg = (name, fallback = null) => { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : fallback; }; if (process.argv.includes("--help")) { process.stdout.write("usage: node tools/web/search-wbv2-batch-source.mjs [--task ] --pattern [--unit ...] [--runtime ...] [--max-hits ] or --list\n"); process.exit(0); } const task = arg("--task") ?? read("docs/EXECUTION_QUEUE.md").match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1]; const pattern = arg("--pattern"); if (!task) throw new Error("unable to resolve current WBV2 task"); if (!pattern && !process.argv.includes("--list")) throw new Error("usage: node tools/web/search-wbv2-batch-source.mjs [--task ] --pattern [--runtime ...] or --list"); const queueTaskManifest = `tests/golden/${task}/manifest.json`; const index = json("tests/golden/WBV2/task-index.json"); const plan = json(index.source.path); const taskRecord = plan.tasks.find((candidate) => candidate.id === task); const manifest = fs.existsSync(abs(queueTaskManifest)) ? json(queueTaskManifest) : null; const sourcePaths = new Set(); for (const artifact of Object.values(manifest?.artifacts ?? {})) { if (artifact?.path?.startsWith("blender-5.2.0/source/blender/")) sourcePaths.add(artifact.path); } for (const source of taskRecord?.module?.sourcePaths ?? []) { if (source.startsWith("blender-5.2.0/source/blender/")) sourcePaths.add(source); } const selectedUnits = []; for (let i = 0; i < process.argv.length; i += 1) { if (process.argv[i] === "--unit") selectedUnits.push(process.argv[++i]); } if (selectedUnits.length > 16) throw new Error("source context is limited to 16 explicit unit IDs"); if (selectedUnits.length) { const inventory = json("tests/golden/WBV2/dynamic-inventory.json"); for (const id of selectedUnits) { const unit = inventory.units.find((item) => item.id === id); if (!unit) throw new Error(`unknown source unit: ${id}`); for (const location of unit.locations ?? []) { if (location.path.startsWith("blender-5.2.0/source/blender/")) sourcePaths.add(location.path); } } } const runtimeAllowlist = new Set([ "tools/web/run-wbv2-module-task.mjs", "tools/web/run-wbv2-continuous.mjs", "tools/web/bind-wbv2-module-coverage.mjs", "tools/web/finalize-wbv2-module-batch.mjs", "tools/web/check-source-dynamic-inventory.mjs", "tools/web/reuse-wbv2-source-dependencies.mjs", "my-tools/wbv2_runtime.py", "web/app/src/engine-client/WebEngineClient.ts", "web/app/src/app/App.tsx", "web/protocol/nonmesh-geometry-commands.ts", "web/protocol/web-engine.ts", "web/protocol/scene-ir.ts", "web/protocol/animation-render-commands.ts", "web/protocol/nla.ts", "web/protocol/paint.ts", "web/protocol/sculpt.ts", "web/app/src/fonts/external-vfont-import.ts", "web/app/src/workers/web-engine.worker.ts", "web/protocol/web-engine-capabilities.ts", "web/protocol/blender-operator.ts", "web/protocol/core-scene-commands.ts", "web/protocol/mesh-authoring-commands.ts", "web/protocol/modifier.ts", "web/protocol/geometry-node-commands.ts", "web/protocol/geometry-nodes.ts", "web/protocol/shader-graph-commands.ts", "web/protocol/shader-graph.ts", "web/protocol/sequencer.ts", "web/protocol/compositor-commands.ts", "web/protocol/physics-simulation.ts", "web/protocol/tracking-mask.ts", "web/protocol/grease-pencil.ts", "web/protocol/render-routing.ts", "web/protocol/render-capabilities.ts", "web/protocol/asset-library-io.ts", "web/protocol/library-main-append.ts", "web/protocol/io-format-ui-gate.ts", "web/protocol/io-media-commands.ts", "web/app/src/sequencer/SequencerTimeline.ts", "web/app/src/capabilities/io-format-ui-registry.json", "blender-5.2.0/source/blender/web_engine/web_engine_api.cpp", "blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp", "blender-5.2.0/source/blender/web_engine/web_engine_main_state.h", ]); const runtimePaths = []; // Product workflows are bounded subtasks of the current authoritative task. const backlogPath = abs("tests/golden/WBV2/web-product-backlog.json"); if (fs.existsSync(backlogPath)) { const backlog = JSON.parse(fs.readFileSync(backlogPath, "utf8")); if (backlog.queueTask === task) { for (const job of backlog.tasks ?? []) { for (const file of job.ownedPaths ?? []) runtimeAllowlist.add(file); } } } for (const file of ["tools/web/deployment-http-server.mjs", "my-tools/wbv2_autopilot.py", "my-tools/run_web_product_migration.py", "tools/web/run-wbv2-scheduler.mjs", "tools/web/run-wbv2-guardian.mjs", "tools/web/check-wbv2-module-execution.mjs"]) runtimeAllowlist.add(file); for (let index = 0; index < process.argv.length; index += 1) { if (process.argv[index] === "--runtime") { const file = process.argv[index + 1]; if (!runtimeAllowlist.has(file)) throw new Error(`runtime path is not allowlisted: ${file}`); runtimePaths.push(file); index += 1; } } const allowedPaths = [...sourcePaths, ...runtimePaths].filter((file, index, all) => all.indexOf(file) === index && fs.existsSync(abs(file))); if (!allowedPaths.length) throw new Error(`no batch-scoped source paths found for ${task}`); if (process.argv.includes("--list")) { process.stdout.write(`${JSON.stringify({ schemaVersion: 1, task, scope: "batch", sourcePaths: [...sourcePaths], runtimePaths, allowedPaths }, null, 2)}\n`); process.exit(0); } const result = spawnSync("rg", ["--line-number", "--no-heading", "--color", "never", pattern, ...allowedPaths], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, }); if (result.error) throw result.error; if (result.status !== 0 && result.status !== 1) throw new Error(`batch search failed with exit ${result.status}`); const output = result.stdout ?? ""; const hits = output ? output.trimEnd().split("\n").filter(Boolean) : []; const maxHits = Number(arg("--max-hits", "60")); if (!Number.isSafeInteger(maxHits) || maxHits < 1 || maxHits > 200) throw new Error("--max-hits must be between 1 and 200"); process.stdout.write(`${JSON.stringify({ schemaVersion: 1, task, scope: "batch", pattern, selectedUnits, sourcePathCount: sourcePaths.size, runtimePathCount: runtimePaths.length, hitCount: hits.length, truncated: hits.length > maxHits, hits: hits.slice(0, maxHits) }, null, 2)}\n`);