diff --git a/README.md b/README.md index b99dc463..edc69b97 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Generic `dependencies`, generic `security`, arbitrary category/language combinat **The published `0.1.2` does not yet implement bundled WASM fallback or automatic conversation delivery.** The existing commands remain the unified entry points; native adapter-specific prerequisites still apply today: -An opt-in source build with `--features codeguard-cli/wasm-precheck` has narrow TypeScript and Java single-file candidate paths. For TypeScript, a visible local ESLint 10 entry and one flat config take priority: when an executable Node is resolved from `PATH`, the CLI runs its existing bounded native probe; without Node it reports the setup gap. If no local entry is visible, the bundled TypeScript/TSX candidate can provide suspected syntax positions. The Java candidate likewise remains incomplete; no bundled grammar has an accepted language/dialect range. Native observations and prechecks never approve delivery. Default and published binaries keep their previous native-context behavior. [TypeScript acceptance](tests/acceptance/typescript-syntax-fallback-candidate.md) · [native-first acceptance](tests/acceptance/native-first-eslint-candidate.md) · [Java acceptance](tests/acceptance/java-syntax-fallback-candidate.md). +An opt-in source build with `--features codeguard-cli/wasm-precheck` has narrow TypeScript and Java single-file candidate paths. For TypeScript, a visible local ESLint 10 entry and one flat config take priority: when an executable Node is resolved from `PATH`, the CLI runs its existing bounded native probe; without Node it reports the setup gap. Only when the project-local ESLint package is not observed can the bundled TypeScript/TSX candidate provide suspected syntax positions; an untrusted entry, package identity or config returns a specific setup blocker. The Java candidate likewise remains incomplete; no bundled grammar has an accepted language/dialect range. Native observations and prechecks never approve delivery. Default and published binaries keep their previous native-context behavior. [TypeScript acceptance](tests/acceptance/typescript-syntax-fallback-candidate.md) · [native-first acceptance](tests/acceptance/native-first-eslint-candidate.md) · [Java acceptance](tests/acceptance/java-syntax-fallback-candidate.md). ```bash codeguard lint java . diff --git a/README.zh-CN.md b/README.zh-CN.md index c41d803d..472d0352 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -180,7 +180,7 @@ Python 编辑快反馈可执行 `codeguard lint python . --file src/changed.py - **公开 `0.1.2` 尚未实现内置 WASM 兜底或自动对话交付。** 保留已有命令作为统一入口;当前仍须满足各原生适配器的参数和前置条件: -可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口,无本地入口时才提供 TypeScript/TSX 疑似语法初检。Java 候选仍须原生确认,所有内置 grammar 的版本/方言尚未验收,原生局部观察和初检都不能批准交付。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md)、[TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)与[原生优先局部验收](tests/acceptance/native-first-eslint-candidate.md)。 +可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口;仅本地 ESLint 包未观察到时才提供 TypeScript/TSX 疑似语法初检。入口、包身份或配置不可信时保留具体环境阻塞。Java 候选仍须原生确认,所有内置 grammar 的版本/方言尚未验收,原生局部观察和初检都不能批准交付。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md)、[TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)与[原生优先局部验收](tests/acceptance/native-first-eslint-candidate.md)。 ```bash codeguard lint java . diff --git a/crates/codeguard-cli/src/eslint_lint_command.rs b/crates/codeguard-cli/src/eslint_lint_command.rs index 3839d452..5eb93981 100644 --- a/crates/codeguard-cli/src/eslint_lint_command.rs +++ b/crates/codeguard-cli/src/eslint_lint_command.rs @@ -43,42 +43,37 @@ pub fn run(args: &[String]) -> ExitCode { } #[cfg(feature = "wasm-precheck")] if crate::typescript_syntax_precheck::eligible(&args) { - if let Some(candidate) = - crate::eslint_native_first_candidate::observed_candidate(&args.source) - { - let mut native_args = args.clone(); - native_args.entry = Some(candidate.entry); - native_args.config = Some(candidate.config); - native_args.cwd = Some(candidate.root); - native_args.version = Some(candidate.version); - let report = if let Some(node) = crate::eslint_native_first_candidate::node_on_path() { - native_args.node = Some(node); - observe_with_preparation(&native_args, deadline) - } else { - observe_with_preparation_reason( - &args, - deadline, - Some("eslint_node_runtime_unresolved"), - ) - }; - if args.json { - println!("{report}"); - } else { - print_feedback(&report); + let report = match crate::eslint_native_first_candidate::observed_candidate(&args.source) { + Ok(Some(candidate)) => { + let mut native_args = args.clone(); + native_args.entry = Some(candidate.entry); + native_args.config = Some(candidate.config); + native_args.cwd = Some(candidate.root); + native_args.version = Some(candidate.version); + if let Some(node) = crate::eslint_native_first_candidate::node_on_path() { + native_args.node = Some(node); + observe_with_preparation(&native_args, deadline) + } else { + observe_with_preparation_reason( + &args, + deadline, + Some("eslint_node_runtime_unresolved"), + ) + } } - return ExitCode::from(if report["reason"] == "request_cancelled" { - 130 - } else { - 3 - }); - } - let report = crate::typescript_syntax_precheck::observe(&args, deadline); + Ok(None) => crate::typescript_syntax_precheck::observe(&args, deadline), + Err(reason) => observe_with_preparation_reason(&args, deadline, Some(reason)), + }; if args.json { println!("{report}"); } else { print_feedback(&report); } - return ExitCode::from(3); + return ExitCode::from(if report["reason"] == "request_cancelled" { + 130 + } else { + 3 + }); } let report = observe_with_preparation(&args, deadline); if args.json { @@ -102,12 +97,11 @@ fn observe_with_preparation_reason( ) -> Value { let mut report = observe(args, deadline); if report["reason"] == "eslint_execution_context_missing" - && missing_context_reason == Some("eslint_node_runtime_unresolved") + && missing_context_reason.is_some_and(crate::eslint_preparation::valid_reason) { - report["reason"] = json!("eslint_node_runtime_unresolved"); - report["next_action"] = json!( - "项目本地 ESLint 候选与原配置已发现;当前未解析到 Node。核对项目运行时并提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码" - ); + let reason = missing_context_reason.unwrap_or("eslint_execution_context_missing"); + report["reason"] = json!(reason); + report["next_action"] = feedback(reason)["next_action"].clone(); } // 早期上下文失败也进入环境待办;取消、外部目标或不支持范围不能制造源码任务。 if let (Some(workspace), Some(reason)) = ( @@ -278,6 +272,14 @@ fn feedback(reason: &str) -> Value { json!({"schema_version":"0.2.0","report_type":"eslint_local_feedback","status":"incomplete","local_coherent":false,"coverage_proven":false,"delivery_decision":"not_evaluated","reason":reason,"findings":[],"suppressed_count":0,"workbench_status":"not_connected","workbench":null,"next_action":match reason { "eslint_execution_context_missing"=>"提供显式 Node、原 ESLint JS 入口、具体版本、原工作目录与项目原 flat config 后复检;不安装或替换规则", "eslint_node_runtime_unresolved"=>"项目本地 ESLint 候选已发现;提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码", + "eslint_project_manifest_untrusted"=>"核对项目 package.json 的文件类型、内容及 ESLint 声明后再选择原生检查;不要先改源码或重复安装", + "eslint_local_dependency_path_untrusted"=>"核对 node_modules 与本地 ESLint 包目录的链接、类型及来源;不能据此判定工具缺失", + "eslint_local_package_identity_invalid"=>"核对本地 ESLint package.json 的名称、版本和入口声明,恢复可信包身份后原生复检", + "eslint_local_entry_untrusted"=>"核对本地 ESLint JS 入口的真实类型与来源,不跟随链接执行;恢复后原生复检", + "eslint_local_version_unresolved"=>"核对项目 ESLint 声明与本地包版本,按原项目方案恢复一致后原生复检", + "eslint_adapter_version_unsupported"=>"项目 ESLint 版本不受当前适配器支持;保留原项目版本与检查义务,补齐适配器或提出具体决策,不修改无依据源码", + "eslint_config_selection_unresolved"=>"项目本地 ESLint 已发现,但 flat config 缺失或存在多份;确认原项目选中的配置后原生复检", + "eslint_config_untrusted"=>"项目本地 ESLint 已发现,但配置是链接、特殊文件或不可读;恢复原配置后原生复检", "eslint_scope_requires_explicit_file"=>"当前入口需显式单文件;项目目录与完整源集调度尚未接通,不将目录标为已检查", "request_cancelled"|"request_deadline_exceeded"=>"重新安排原工具检查预算;中断不要求修改源码", _=>"核对原配置、parser/插件与本轮规则;按原生发现修复后用同一入口和配置复检,抑制及未知覆盖需核查,任务不能自行关闭", diff --git a/crates/codeguard-cli/src/eslint_native_first_candidate.rs b/crates/codeguard-cli/src/eslint_native_first_candidate.rs index 991ecc1e..39ef3631 100644 --- a/crates/codeguard-cli/src/eslint_native_first_candidate.rs +++ b/crates/codeguard-cli/src/eslint_native_first_candidate.rs @@ -4,6 +4,7 @@ use codeguard_adapters::{EslintConfigState, inspect_eslint_local_candidate}; use codeguard_core::ObservedPathKind; use codeguard_runtime::read_bounded_regular_file; use std::{ + io::ErrorKind, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, }; @@ -21,27 +22,41 @@ pub(crate) struct LocalEslintCandidate { } /// 识别源码最近项目根的本地 ESLint 包、入口和单一 flat config。 -/// 参数为普通源码路径;返回候选仅表示可以尝试原生版本探测,不证明规则有效。 -pub(crate) fn observed_candidate(source: &Path) -> Option { - let source = source.canonicalize().ok()?; - let parent = source.parent()?; +/// 参数为普通源码路径;返回 Ok(None) 仅表示本地入口未观察到,错误表示必须保留的环境阻塞。 +pub(crate) fn observed_candidate( + source: &Path, +) -> Result, &'static str> { + let source = source + .canonicalize() + .map_err(|_| "eslint_source_unavailable")?; + let parent = source.parent().ok_or("eslint_source_unavailable")?; for root in parent.ancestors() { let manifest = root.join("package.json"); - if !regular_file(&manifest) { - continue; + match std::fs::symlink_metadata(&manifest) { + Err(error) if error.kind() == ErrorKind::NotFound => continue, + Ok(metadata) if metadata.is_file() => {} + _ => return Err("eslint_project_manifest_untrusted"), } - let project_manifest = read_bounded_regular_file(&manifest, 256 * 1024).ok()?; + let project_manifest = read_bounded_regular_file(&manifest, 256 * 1024) + .map_err(|_| "eslint_project_manifest_untrusted")?; let local_root = root.join("node_modules"); let package_root = local_root.join("eslint"); - if !regular_directory(&local_root) || !regular_directory(&package_root) { - return None; + if !directory_or_absent(&local_root)? || !directory_or_absent(&package_root)? { + return Ok(None); } let local_manifest = package_root.join("package.json"); + let bin_root = package_root.join("bin"); let entry = package_root.join("bin/eslint.js"); - if !regular_file(&local_manifest) || !regular_file(&entry) { - return None; + if !regular_file(&local_manifest) { + return Err("eslint_local_package_identity_invalid"); + } + let local_manifest = read_bounded_regular_file(&local_manifest, 256 * 1024) + .map_err(|_| "eslint_local_package_identity_invalid")?; + if !std::fs::symlink_metadata(&bin_root).is_ok_and(|metadata| metadata.is_dir()) + || !regular_file(&entry) + { + return Err("eslint_local_entry_untrusted"); } - let local_manifest = read_bounded_regular_file(&local_manifest, 256 * 1024).ok()?; // 旧配置、TS loader 与多配置选择尚未受控,不能构造自动原生命令。 let configs: Vec<_> = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"] .iter() @@ -49,10 +64,10 @@ pub(crate) fn observed_candidate(source: &Path) -> Option .filter(|path| std::fs::symlink_metadata(path).is_ok()) .collect(); let [config] = configs.as_slice() else { - return None; + return Err("eslint_config_selection_unresolved"); }; if !regular_file(config) || read_bounded_regular_file(config, 1024 * 1024).is_err() { - return None; + return Err("eslint_config_untrusted"); } let candidate = inspect_eslint_local_candidate( Some(&project_manifest), @@ -61,16 +76,31 @@ pub(crate) fn observed_candidate(source: &Path) -> Option EslintConfigState::Observed, ); if candidate.state != "local_candidate_requires_native_probe" { - return None; + return Err(match candidate.state { + "project_manifest_invalid" => "eslint_project_manifest_untrusted", + "local_package_identity_invalid" => "eslint_local_package_identity_invalid", + "local_version_not_supported_by_adapter" => "eslint_adapter_version_unsupported", + _ => "eslint_local_version_unresolved", + }); } - return Some(LocalEslintCandidate { + return Ok(Some(LocalEslintCandidate { root: root.to_path_buf(), entry, config: config.clone(), - version: candidate.observed_version?, - }); + version: candidate + .observed_version + .ok_or("eslint_local_version_unresolved")?, + })); + } + Ok(None) +} + +fn directory_or_absent(path: &Path) -> Result { + match std::fs::symlink_metadata(path) { + Ok(metadata) if metadata.is_dir() => Ok(true), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(false), + _ => Err("eslint_local_dependency_path_untrusted"), } - None } /// 从当前进程 PATH 选择普通可执行 Node 文件;缺失只表示当前调用未解析到运行时。 @@ -100,7 +130,3 @@ pub(crate) fn node_on_path() -> Option { fn regular_file(path: &Path) -> bool { std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_file()) } - -fn regular_directory(path: &Path) -> bool { - std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir()) -} diff --git a/crates/codeguard-cli/src/eslint_preparation.rs b/crates/codeguard-cli/src/eslint_preparation.rs index 95d80475..953bb122 100644 --- a/crates/codeguard-cli/src/eslint_preparation.rs +++ b/crates/codeguard-cli/src/eslint_preparation.rs @@ -143,6 +143,14 @@ pub(crate) fn valid_reason(reason: &str) -> bool { reason, "eslint_execution_context_missing" | "eslint_node_runtime_unresolved" + | "eslint_project_manifest_untrusted" + | "eslint_local_dependency_path_untrusted" + | "eslint_local_package_identity_invalid" + | "eslint_local_entry_untrusted" + | "eslint_local_version_unresolved" + | "eslint_adapter_version_unsupported" + | "eslint_config_selection_unresolved" + | "eslint_config_untrusted" | "eslint_input_unavailable" | "eslint_private_workspace_unavailable" | "request_deadline_exceeded" @@ -293,10 +301,29 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value { { return fallback; } - let step = if report["diagnostic_reason"] == "eslint_node_runtime_unresolved" { - "项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码" - } else { - "核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码" + let step = match report["diagnostic_reason"].as_str() { + Some("eslint_node_runtime_unresolved") => { + "项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码" + } + Some("eslint_config_selection_unresolved" | "eslint_config_untrusted") => { + "项目本地 ESLint 已发现;核对原项目实际选中的 flat config、文件类型与来源,恢复配置后原生复检,不修改无依据源码" + } + Some("eslint_local_entry_untrusted" | "eslint_local_dependency_path_untrusted") => { + "项目本地 ESLint 路径不可信;核对链接、文件类型和来源后原生复检,不重复安装或修改无依据源码" + } + Some("eslint_adapter_version_unsupported") => { + "项目 ESLint 版本不受当前适配器支持;保留原项目版本与检查义务,补齐适配器或提出具体决策,不修改无依据源码" + } + Some( + "eslint_local_package_identity_invalid" + | "eslint_local_version_unresolved" + | "eslint_project_manifest_untrusted", + ) => { + "项目本地 ESLint 身份或版本声明无法核对;检查 package.json 与原依赖方案,恢复一致后原生复检,不修改无依据源码" + } + _ => { + "核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码" + } }; json!({"disposition":"actionable","step":step,"diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha}) } diff --git a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs index ff023306..863f46f7 100644 --- a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs +++ b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs @@ -288,6 +288,130 @@ fn unresolved_node_updates_one_stable_environment_task_with_specific_guidance() } } +#[test] +fn ambiguous_or_untrusted_local_eslint_is_a_setup_blocker_not_wasm_source_finding() { + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + let source = root.join("bad.ts"); + fs::write(&source, "const value: number = ;\n").unwrap(); + fs::write( + root.join("package.json"), + r#"{"devDependencies":{"eslint":"10.0.0"}}"#, + ) + .unwrap(); + fs::create_dir_all(root.join("node_modules/eslint/bin")).unwrap(); + fs::write( + root.join("node_modules/eslint/package.json"), + r#"{"name":"eslint","version":"10.0.0","bin":{"eslint":"bin/eslint.js"}}"#, + ) + .unwrap(); + let entry = root.join("node_modules/eslint/bin/eslint.js"); + fs::write(&entry, "fixture\n").unwrap(); + fs::write(root.join("eslint.config.js"), "export default [];\n").unwrap(); + fs::write(root.join("eslint.config.mjs"), "export default [];\n").unwrap(); + let initialized = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "init", + root.to_str().unwrap(), + "--apply", + "--format", + "json", + ]) + .output() + .unwrap(); + assert_eq!(initialized.status.code(), Some(3)); + let lint = || { + let output = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "lint", + "typescript", + source.to_str().unwrap(), + "--workspace", + root.to_str().unwrap(), + "--format", + "json", + ]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(3)); + serde_json::from_slice::(&output.stdout).unwrap() + }; + let ambiguous = lint(); + assert_eq!(ambiguous["reason"], "eslint_config_selection_unresolved"); + assert!(ambiguous.get("syntax_precheck").is_none()); + assert!(ambiguous["findings"].as_array().unwrap().is_empty()); + assert_eq!( + ambiguous["workbench_status"], "synced_partial", + "{ambiguous}" + ); + assert_eq!(ambiguous["workbench"]["new_blockers"], 1); + assert!( + ambiguous["next_action"] + .as_str() + .unwrap() + .contains("flat config") + ); + let task_id = ambiguous["workbench"]["next"]["repair_brief"]["task_id"].clone(); + + fs::remove_file(root.join("eslint.config.mjs")).unwrap(); + fs::remove_file(&entry).unwrap(); + std::os::unix::fs::symlink(root.join("eslint.config.js"), &entry).unwrap(); + let untrusted = lint(); + assert_eq!(untrusted["reason"], "eslint_local_entry_untrusted"); + assert!(untrusted.get("syntax_precheck").is_none()); + assert!(untrusted["findings"].as_array().unwrap().is_empty()); + assert_eq!( + untrusted["workbench_status"], "synced_partial", + "{untrusted}" + ); + assert_eq!(untrusted["workbench"]["new_blockers"], 0); + assert_eq!( + untrusted["workbench"]["next"]["repair_brief"]["task_id"], + task_id + ); + assert!( + untrusted["next_action"] + .as_str() + .unwrap() + .contains("路径不可信") + ); + + fs::remove_file(&entry).unwrap(); + fs::write(&entry, "fixture\n").unwrap(); + fs::write(root.join("node_modules/eslint/package.json"), b"{bad-json").unwrap(); + let invalid_identity = lint(); + assert_eq!( + invalid_identity["reason"], + "eslint_local_package_identity_invalid" + ); + assert!(invalid_identity.get("syntax_precheck").is_none()); + assert_eq!(invalid_identity["workbench"]["new_blockers"], 0); + assert_eq!( + invalid_identity["workbench"]["next"]["repair_brief"]["task_id"], + task_id + ); + + fs::write( + root.join("package.json"), + r#"{"devDependencies":{"eslint":"9.0.0"}}"#, + ) + .unwrap(); + fs::write( + root.join("node_modules/eslint/package.json"), + r#"{"name":"eslint","version":"9.0.0","bin":{"eslint":"bin/eslint.js"}}"#, + ) + .unwrap(); + let unsupported = lint(); + assert_eq!(unsupported["reason"], "eslint_adapter_version_unsupported"); + assert!(unsupported.get("syntax_precheck").is_none()); + assert!( + unsupported["next_action"] + .as_str() + .unwrap() + .contains("补齐适配器") + ); +} + #[test] fn configured_but_uninstalled_eslint_still_allows_candidate_precheck() { let project = Project::new(); diff --git a/docs/Codeguard-Command-Reference.md b/docs/Codeguard-Command-Reference.md index 10bb611d..c252fba6 100644 --- a/docs/Codeguard-Command-Reference.md +++ b/docs/Codeguard-Command-Reference.md @@ -10,7 +10,7 @@ The command catalog below defines responsibility, not availability. Current disp Current slices include static discovery/init, selected native checks, partial aggregation, work sync/next, local attempts/leases, selected original-tool rechecks and candidate inspection. Current doctor takes `[path]`; Java doclint is selected through `lint java --checker javadoc`. Do not assume a public `lint rust`, generic fix, complete gate or MCP service exists because its target is described here. A callable recheck does not imply final verified closure. -The opt-in `codeguard-cli/wasm-precheck` build adds narrow `lint typescript ` and `lint java ` paths when no explicit native execution context is provided. TypeScript first recognizes an ordinary project-local ESLint 10 entry and one flat config, then uses an executable Node from `PATH` to run the existing bounded native probe; when Node is unresolved, it reports setup instead of starting WASM. Without a local entry, it returns [TypeScript feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) or TSX 0.4.0 candidate feedback. Java returns [Java feedback 0.1.0](../schemas/java-syntax-precheck-feedback-v0.1.schema.json). These paths retain exit 3 and never turn an unvalidated grammar or local native observation into delivery approval. Default/published binaries retain the existing native-context behavior. See [native-first acceptance](../tests/acceptance/native-first-eslint-candidate.md). +The opt-in `codeguard-cli/wasm-precheck` build adds narrow `lint typescript ` and `lint java ` paths when no explicit native execution context is provided. TypeScript first recognizes an ordinary project-local ESLint 10 entry and one flat config, then uses an executable Node from `PATH` to run the existing bounded native probe; when Node is unresolved, it reports setup instead of starting WASM. Only when no project-local ESLint package is observed does it return [TypeScript feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) or TSX 0.4.0 candidate feedback. Untrusted local paths, package identity or config selection remain setup blockers. Java returns [Java feedback 0.1.0](../schemas/java-syntax-precheck-feedback-v0.1.schema.json). These paths retain exit 3 and never turn an unvalidated grammar or local native observation into delivery approval. Default/published binaries retain the existing native-context behavior. See [native-first acceptance](../tests/acceptance/native-first-eslint-candidate.md). `codeguard hook plan [--format=json]` is a current, read-only host-event bridge. It reads a bounded [1.0 request](../schemas/hook-trigger-request.schema.json) from stdin and returns a [1.1 candidate tier](../schemas/hook-trigger-plan-v1.1.schema.json) with exit 3, `execution=not_run` and `delivery_decision=not_evaluated`. A confirmed edit may request fast file checks for at most eight distinct paths, 512 bytes per path and 2 KiB combined; an over-budget event instead requests scope resolution with `fast_scope_budget_exceeded`, without silently dropping files. This is a signal to select a bounded batch check, not to resubmit the same list forever. It does not invoke a checker, take a Git snapshot or block any host. The earlier [1.0 response schema](../schemas/hook-trigger-plan.schema.json) remains available for historical readers. See the [CLI acceptance record](../tests/acceptance/hook-plan-cli-candidate.md). diff --git a/docs/Codeguard-Command-Reference.zh_CN.md b/docs/Codeguard-Command-Reference.zh_CN.md index 04bc9a07..890eb77a 100644 --- a/docs/Codeguard-Command-Reference.zh_CN.md +++ b/docs/Codeguard-Command-Reference.zh_CN.md @@ -20,7 +20,7 @@ | gate | 仅 pre-commit 路径安全预览;不是完整内容门禁 | | fix/pre-push/ci/mcp/compat/dependencies/security | 目标通用入口未实现,不能复制目标例子直接使用 | -[已验收实现切片](../openspec/changes/introduce-rust-codeguard-cli/implementation-baseline.md)跟踪当前局部能力;[tasks](../openspec/changes/introduce-rust-codeguard-cli/tasks.md)独占完成状态。WASM 的完整统一入口仍为 S14 待办。可选 `codeguard-cli/wasm-precheck` 构建处理 `lint typescript <显式单文件>` 时,若观察到本地 ESLint 10 入口与唯一 flat config,会优先用 `PATH` 中可执行 Node 调用既有有界原生探针;缺 Node 则返回准备缺口。无可见本地入口时才输出 [TypeScript 0.3.0 候选反馈](../schemas/eslint-local-feedback-v0.3.schema.json)或 TSX 0.4.0。`lint java <显式单文件>` 仍输出 [Java 0.1.0 候选反馈](../schemas/java-syntax-precheck-feedback-v0.1.schema.json)。这些局部路径均不批准交付并保持退出 3;部分显式参数、Javadoc/Checkstyle 和符号链接沿用原路径,默认及已发布二进制也不含这项自动调度。见[原生优先局部验收](../tests/acceptance/native-first-eslint-candidate.md)与技术方案 5.3–5.4;后文未标当前的语法、退出0/1与闭环承诺均为目标契约。 +[已验收实现切片](../openspec/changes/introduce-rust-codeguard-cli/implementation-baseline.md)跟踪当前局部能力;[tasks](../openspec/changes/introduce-rust-codeguard-cli/tasks.md)独占完成状态。WASM 的完整统一入口仍为 S14 待办。可选 `codeguard-cli/wasm-precheck` 构建处理 `lint typescript <显式单文件>` 时,若观察到本地 ESLint 10 入口与唯一 flat config,会优先用 `PATH` 中可执行 Node 调用既有有界原生探针;缺 Node 则返回准备缺口。仅未观察到本地 ESLint 包时才输出 [TypeScript 0.3.0 候选反馈](../schemas/eslint-local-feedback-v0.3.schema.json)或 TSX 0.4.0。本地入口、包身份或配置不可信时保留具体环境阻塞,不启动 WASM。`lint java <显式单文件>` 仍输出 [Java 0.1.0 候选反馈](../schemas/java-syntax-precheck-feedback-v0.1.schema.json)。这些局部路径均不批准交付并保持退出 3;部分显式参数、Javadoc/Checkstyle 和符号链接沿用原路径,默认及已发布二进制也不含这项自动调度。见[原生优先局部验收](../tests/acceptance/native-first-eslint-candidate.md)与技术方案 5.3–5.4;后文未标当前的语法、退出0/1与闭环承诺均为目标契约。 ## 1. 命令体系与协作路线 diff --git a/docs/Codeguard-Technical-Design.md b/docs/Codeguard-Technical-Design.md index e3bdbe93..b4f2ea67 100644 --- a/docs/Codeguard-Technical-Design.md +++ b/docs/Codeguard-Technical-Design.md @@ -185,7 +185,7 @@ When some files have suspected issues and others are unresolved, use overall `in These are the target semantic fields, not a shipped project-wide lint/check report. A narrow [candidate precheck schema](../schemas/syntax-precheck-candidate.schema.json) and [strict Rust reader](../crates/codeguard-adapters/src/syntax_precheck_candidate_report.rs) bind source SHA-256, pinned grammar identity and file dialect, recalculate status, and reject unknown versions or forged `clean`. Optional CLI single-file [TypeScript](../schemas/eslint-local-feedback-v0.3.schema.json), [TSX](../schemas/eslint-local-feedback-v0.4.schema.json) and [Java](../schemas/java-syntax-precheck-feedback-v0.1.schema.json) producers include recovery positions and setup guidance; host rendering and durable task references remain absent. The bundled grammars remain unvalidated, so these reports cannot return `clean`. Missing required native execution remains incomplete (`3`); a suspected parser issue alone is not a confirmed violation (`1`). If a dedicated syntax-only operation is later introduced, its success must be explicitly scoped to syntax; no new `syntax` command is claimed here. -The opt-in CLI build first checks for a local ESLint 10 package and a single ordinary flat config on single-file TypeScript requests without explicit native context. With an executable Node resolved from `PATH`, it invokes the existing bounded native version/report probe; without Node it reports the preparation gap rather than running WASM first. If no local entry is visible, the CLI emits [ESLint feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) for `.ts/.mts/.cts` or [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json) for `.tsx`. TSX uses its own pinned grammar. Candidate prechecks preserve `native=not_run` and `delivery=not_evaluated`, and remain incomplete even with zero recoveries. A partial explicit context or symlink does not trigger this route. This is a single-file slice, not project script equivalence, per-module orchestration or automatic host feedback; see the [native-first acceptance](../tests/acceptance/native-first-eslint-candidate.md). +The opt-in CLI build first checks for a local ESLint 10 package and a single ordinary flat config on single-file TypeScript requests without explicit native context. With an executable Node resolved from `PATH`, it invokes the existing bounded native version/report probe; without Node it reports the preparation gap rather than running WASM first. Only when no project-local ESLint package is observed does the CLI emit [ESLint feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) for `.ts/.mts/.cts` or [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json) for `.tsx`. TSX uses its own pinned grammar. Candidate prechecks preserve `native=not_run` and `delivery=not_evaluated`, and remain incomplete even with zero recoveries. An ambiguous config, untrusted local path or invalid package identity remains a setup blocker and does not trigger WASM; a partial explicit context also retains its existing path. This is a single-file slice, not project script equivalence, per-module orchestration or automatic host feedback; see the [native-first acceptance](../tests/acceptance/native-first-eslint-candidate.md). ### 5.4 Grammar import and runtime lifecycle — target diff --git a/docs/Codeguard-Technical-Design.zh_CN.md b/docs/Codeguard-Technical-Design.zh_CN.md index e5edb93b..efd8cff3 100644 --- a/docs/Codeguard-Technical-Design.zh_CN.md +++ b/docs/Codeguard-Technical-Design.zh_CN.md @@ -185,7 +185,7 @@ flowchart LR 这些是目标语义字段,尚非已发行的项目级 lint/check 报告。局部的[候选初检 schema](../schemas/syntax-precheck-candidate.schema.json) 与 [Rust 严格读者](../crates/codeguard-adapters/src/syntax_precheck_candidate_report.rs)已绑定源码 SHA-256、固定 grammar 身份和文件方言,重新计算状态,并拒绝未知版本及伪造的 `clean`。可选 CLI 的 [TypeScript](../schemas/eslint-local-feedback-v0.3.schema.json)、[TSX](../schemas/eslint-local-feedback-v0.4.schema.json) 与 [Java](../schemas/java-syntax-precheck-feedback-v0.1.schema.json) 单文件生产端已提供恢复位置和准备指引;宿主渲染与持久任务引用仍缺。内置 grammar 仍是未验收候选,因此不能返回 `clean`。保持既有退出语义:必需原生执行缺失仍为未完成(`3`);解析器疑似问题本身不是已确认违规(`1`)。若未来增加独立语法操作,其成功必须限定为语法初检;本文不宣称已有 `syntax` 命令。 -可选特性构建的 CLI 在无显式原生上下文的 TypeScript 单文件请求中,先核对本地 ESLint 10 包及唯一普通 flat config。若从 `PATH` 解析到可执行 Node,就调用既有有界原生版本与报告探针;缺 Node 时报告准备缺口,不让 WASM 抢跑。没有可见本地入口时,`.ts/.mts/.cts` 输出 [ESLint 反馈 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json),`.tsx` 用独立 grammar 输出 [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json)。候选初检保留 `native=not_run`、`delivery=not_evaluated`,即使零恢复节点也保持未完成。部分显式上下文或符号链接不触发该路径。该增量不证明项目脚本参数等价、逐模块调度或宿主对话交付,见[原生优先局部验收](../tests/acceptance/native-first-eslint-candidate.md)。 +可选特性构建的 CLI 在无显式原生上下文的 TypeScript 单文件请求中,先核对本地 ESLint 10 包及唯一普通 flat config。若从 `PATH` 解析到可执行 Node,就调用既有有界原生版本与报告探针;缺 Node 时报告准备缺口,不让 WASM 抢跑。仅未观察到本地 ESLint 包时,`.ts/.mts/.cts` 输出 [ESLint 反馈 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json),`.tsx` 用独立 grammar 输出 [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json)。候选初检保留 `native=not_run`、`delivery=not_evaluated`,即使零恢复节点也保持未完成。配置选择歧义、本地路径不可信或包身份损坏时保留环境阻塞而不启动 WASM;部分显式上下文也沿用原路径。该增量不证明项目脚本参数等价、逐模块调度或宿主对话交付,见[原生优先局部验收](../tests/acceptance/native-first-eslint-candidate.md)。 ### 5.4 Grammar 引入与运行生命周期——目标 diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index 9a846892..fc9a96c9 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -1032,12 +1032,12 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30 - [ ] 14.2 Rust WASM 加载器。责任:runtime。规格:SP04,SP05。依赖:14.1。验收:支持固定资产离线加载;不依赖 Node 解析器、CodeGraph 或用户安装 tree-sitter-cli;通过有效/损坏/错 ABI 样本及 MSRV。进行中:可选 `wasm-precheck` 特性已通过 Java/TypeScript/TSX Rust `WasmStore` 加载与反例测试,并由私有工作进程从 stdin 处理真实源码、父进程核对身份与恢复锚点,见[局部验收](../../../tests/acceptance/syntax-worker-candidate.md);本机无 Rust 1.85,MSRV 实编及正式 `lint/check` 接线尚缺。 - [ ] 14.3 进程资源隔离。责任:runtime。规格:SP05。依赖:14.2。验收:验证超时、内存、输出、取消与崩溃;损坏 grammar 不阻塞其他模块、不遗留进程。进行中:Unix 候选 worker 复用受控进程组内核,输入 1 MiB、输出 64 KiB,父进程有共同截止时间和取消;崩溃后代清理、输出洪泛、运行中取消及随后正常解析均增加真实子进程反例。Linux 2 GiB `RLIMIT_AS`、3 GiB 分配负例与 worker 故障隔离在 [Ubuntu CI](https://github.com/full-stack-plugins/codeguard/actions/runs/36478461118) 真实通过;Wasmtime 虚拟预留缩小。macOS 本机硬限制实测不支持,候选仍不具内存隔离权威;见[局部验收](../../../tests/acceptance/syntax-worker-candidate.md)。其它目标平台、损坏 grammar 隔离及正式调度仍缺,不勾选。 - [ ] 14.4 语言覆盖与源码映射。责任:adapters。规格:SP06。依赖:14.2。验收:识别 ERROR/MISSING,合并恢复节点;测试 Unicode、换行、嵌入语言、新语法和未知版本,不将不支持当源码违规。进行中:Rust 已提取两类原始节点,adapters 核对 UTF-8/CRLF 字节锚点并转换 Unicode 标量列;结构祖先归组保留并列错误,预算截断保持不完整;[局部证据](../../../tests/acceptance/syntax-recovery-candidate.md)。级联归并语料、版本/嵌入语言及原生对照仍缺。 -- [ ] 14.5 原生工具准备探测。责任:adapters。规格:SP01,SP03。依赖:现有配置发现。验收:识别项目本地工具、wrapper、版本和无效配置;PATH 缺失不等于未安装,不对坏配置重复建议安装。进行中:ESLint 本地包/入口及 Maven Wrapper 脚本/发行配置已有只读候选分类,`detect` 与 `check` 已输出固定路径观察的候选;`node_modules` 普通源码遍历被排除,链接和损坏配置保留阻塞;发现协议升级为 0.4.0,聚合协议升级为 0.31.0 并保留旧 schema;[局部证据](../../../tests/acceptance/native-tool-readiness-candidates.md)。单文件 TypeScript 本地候选已接通受控原生版本探针,见 [原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md);显式工具自动选择、其它语言及多模块原生优先调度、更多构建器和真实多平台验收尚缺,不勾选。 -- [ ] 14.6 逐模块原生优先调度。责任:CLI/core。规格:SP01。依赖:14.4,14.5。验收:原生可用先执行;原生违规不得被 WASM 洗白;多根分别选择并保留运行失败与兄弟结果。进行中:可选 CLI 特性在 `lint typescript`、`lint java` 显式单文件且完全缺少原生上下文时附加候选语法观察,部分原生上下文、符号链接、其它扩展名及 Javadoc/Checkstyle 选择保留原路径,见 [TypeScript 局部验收](../../../tests/acceptance/typescript-syntax-fallback-candidate.md)、[Java 局部验收](../../../tests/acceptance/java-syntax-fallback-candidate.md)。新增 `.tsx` 独立 grammar 的有效/无效 JSX 反例与候选报告方言绑定,见 [TSX 纠错验收](../../../tests/acceptance/tsx-grammar-candidate.md)。本地 ESLint 10 候选与单一 flat config 可见且 PATH 提供普通 Node 时,单文件 TypeScript 路径现在自动调用既有原生探针并保留其局部发现或版本失败;Node 不可解析时保持准备反馈,不启动 WASM;只声明/配置但无本地入口仍可初检,见 [本地原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。项目原脚本参数、多模块调度、真实 ESLint 10 与混合结果保留仍缺,不勾选。 +- [ ] 14.5 原生工具准备探测。责任:adapters。规格:SP01,SP03。依赖:现有配置发现。验收:识别项目本地工具、wrapper、版本和无效配置;PATH 缺失不等于未安装,不对坏配置重复建议安装。进行中:ESLint 本地包/入口及 Maven Wrapper 脚本/发行配置已有只读候选分类,`detect` 与 `check` 已输出固定路径观察的候选;`node_modules` 普通源码遍历被排除,链接和损坏配置保留阻塞;发现协议升级为 0.4.0,聚合协议升级为 0.31.0 并保留旧 schema;[局部证据](../../../tests/acceptance/native-tool-readiness-candidates.md)。单文件 TypeScript 本地候选已接通受控原生版本探针,见 [原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md);单文件 TypeScript 现进一步区分本地包确实未观察到与配置歧义、链接入口、包身份损坏等环境阻塞;阻塞不被误当成工具缺失而触发 WASM,见同一[原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。显式工具自动选择、其它语言及多模块原生优先调度、更多构建器和真实多平台验收尚缺,不勾选。 +- [ ] 14.6 逐模块原生优先调度。责任:CLI/core。规格:SP01。依赖:14.4,14.5。验收:原生可用先执行;原生违规不得被 WASM 洗白;多根分别选择并保留运行失败与兄弟结果。进行中:可选 CLI 特性在 `lint typescript`、`lint java` 显式单文件且完全缺少原生上下文时附加候选语法观察,部分原生上下文、符号链接、其它扩展名及 Javadoc/Checkstyle 选择保留原路径,见 [TypeScript 局部验收](../../../tests/acceptance/typescript-syntax-fallback-candidate.md)、[Java 局部验收](../../../tests/acceptance/java-syntax-fallback-candidate.md)。新增 `.tsx` 独立 grammar 的有效/无效 JSX 反例与候选报告方言绑定,见 [TSX 纠错验收](../../../tests/acceptance/tsx-grammar-candidate.md)。本地 ESLint 10 候选与单一 flat config 可见且 PATH 提供普通 Node 时,单文件 TypeScript 路径现在自动调用既有原生探针并保留其局部发现或版本失败;Node 不可解析或本地入口/配置/包身份不可信时保持具体准备反馈,不启动 WASM;本地包确实未观察到时仍可初检,见 [本地原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。项目原脚本参数、多模块调度、真实 ESLint 10 与混合结果保留仍缺,不勾选。 - [ ] 14.7 初检协议与 schema。责任:core/CLI。规格:SP02,SP11。依赖:14.4。验收:固定版本和状态枚举、输入/资产身份、范围及缺口;空文件集、部分解析和未知版本不能 clean;未知 major 拒绝。进行中:core 已有状态聚合及[局部证据](../../../tests/acceptance/syntax-precheck-status-candidate.md);adapters 候选报告 1.0.0 的封闭 schema 与严格读者拒绝重复键、未知版本、身份漂移与伪造 clean,见[候选报告证据](../../../tests/acceptance/syntax-precheck-report-candidate.md);可选 CLI `lint typescript` 与 `lint java` 单文件入口分别产生 TypeScript 0.3.0、TSX 0.4.0、Java 0.1.0 局部候选反馈及位置化疑似观察,见 [TypeScript](../../../tests/acceptance/typescript-syntax-fallback-candidate.md) 与 [Java](../../../tests/acceptance/java-syntax-fallback-candidate.md) 验收。正式全范围/嵌入区域/排除原因、持久化任务引用、所有消费者未知 major 拒绝与发布兼容仍缺,不勾选。 - [ ] 14.8 安装建议与必须确认策略。责任:core。规格:SP03。依赖:14.6,14.7。验收:可选原生缺失且完整 clean 仅推荐;疑似或已有必需义务要求恢复原生确认;无适配器给具体决策。进行中:core 已有纯决策函数,区分缺工具、坏配置、执行失败和无适配器;仅非空、完整、已验收且无恢复节点的 clean 才允许可选建议,伪造 clean 状态不能降级。见[局部验收](../../../tests/acceptance/syntax-setup-guidance-candidate.md)。真实工具准备来源、CLI/工作台任务投影及宿主反馈尚未接线,不勾选。 - [ ] 14.9 四类可读与 JSON 报告。责任:CLI。规格:SP07,SP11。依赖:14.7,14.8。验收:覆盖原生成功、缺原生且 clean、疑似语法、初检不可用;结果范围/行动/交付语义一致;不伪造任务 ID。进行中:启用可选 WASM 特性的 TypeScript 与 Java 单文件在无显式原生上下文时提供 JSON/human 候选疑似观察;无恢复节点仍为 incomplete,任务引用为空;Java 超大输入保留 worker 未完成,见 [TypeScript](../../../tests/acceptance/typescript-syntax-fallback-candidate.md) 与 [Java](../../../tests/acceptance/java-syntax-fallback-candidate.md) 局部验收。正式四类报告、多模块范围与宿主渲染仍缺,不勾选。 -- [ ] 14.10 稳定任务与下一步。责任:core/CLI。规格:SP08。依赖:14.8,14.9及现有工作台。验收:重复扫描更新同一准备/确认任务,失败尝试有记录,无进展给具体诊断;推荐项不持续占用 next。进行中:单文件 TypeScript 已能把“本地 ESLint 可见但当前 PATH 无 Node”记为 `eslint_node_runtime_unresolved`,初始化工作区连续两轮仅更新同一环境任务,`next` 要求受控 Node 原生复检而非重复安装 ESLint 或修改源码;见[原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。WASM 疑似观察到确认任务的关联、失败尝试/无进展预算、推荐项非阻断投影和多模块任务尚缺,不勾选。 +- [ ] 14.10 稳定任务与下一步。责任:core/CLI。规格:SP08。依赖:14.8,14.9及现有工作台。验收:重复扫描更新同一准备/确认任务,失败尝试有记录,无进展给具体诊断;推荐项不持续占用 next。进行中:单文件 TypeScript 已能把“本地 ESLint 可见但当前 PATH 无 Node”记为 `eslint_node_runtime_unresolved`,初始化工作区连续两轮仅更新同一环境任务,`next` 要求受控 Node 原生复检而非重复安装 ESLint 或修改源码;见[原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。配置歧义、链接入口和损坏包清单连续出现时亦更新同一 ESLint 环境任务、变更具体诊断而不生成源码 finding;见同一[局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。WASM 疑似观察到确认任务的关联、失败尝试/无进展预算、推荐项非阻断投影和多模块任务尚缺,不勾选。 - [ ] 14.11 能力匹配复检。责任:core/adapters。规格:SP08。依赖:14.10。验收:同模块/输入/方言的原生语法能力确认才能关闭;仅安装、后续 WASM clean 或格式检查不能关闭;复发重开。 - [ ] 14.12 精确误报纠错。责任:core。规格:SP09。依赖:14.11及既有白名单提案。验收:绑定 grammar/规则/范围/版本和可信审批,变更失效;原生反证保留历史并进语料,不撤销原生义务。 - [ ] 14.13 缓存与失效恢复。责任:runtime/core。规格:SP10。依赖:14.7。验收:输入、grammar、runtime、规则和配置变化失效;不缓存不完整为 clean;升级回滚不丢历史。 diff --git a/tests/acceptance/native-first-eslint-candidate.md b/tests/acceptance/native-first-eslint-candidate.md index 3c2ab1ed..3d76897c 100644 --- a/tests/acceptance/native-first-eslint-candidate.md +++ b/tests/acceptance/native-first-eslint-candidate.md @@ -4,8 +4,10 @@ 若能解析到 Node,CLI 使用本地包清单中的具体版本、项目入口/配置和构建根调用现有受控原生探针;Node、入口、配置和源码均由探针按字节核对,版本输出与报告范围必须吻合。探针得到的发现仅是局部原生观察,仍为 `delivery_decision=not_evaluated`。若当前 `PATH` 无可用 Node,反馈给出 `eslint_node_runtime_unresolved`,不把 PATH 缺失当作项目工具未安装,也不启动 WASM 抢跑。已初始化的工作区把它同步为原有 ESLint 环境任务;两轮扫描复用同一任务,`next` 明确要求提供受控 Node 路径,避免重复安装 ESLint 或修改无依据源码。项目只声明 ESLint、没有本地入口时仍可提供候选语法初检。 +本地 ESLint 包目录已存在但入口是链接、包身份损坏、项目清单不可信、原 flat config 缺失/多份/不可信或版本声明不一致时,选择器返回具体环境阻塞,不把它们归为“未安装”并启动 WASM。项目版本不受当前适配器支持时返回 `eslint_adapter_version_unsupported`,要求补齐适配器或提出具体决策,不能暗示更改项目版本。只有项目本地包目录确实未观察到时才走候选语法初检。项目已初始化时,这些诊断沿用同一 ESLint 环境任务;配置歧义、链接入口、损坏包清单连续出现时只更新该任务的诊断与下一步,不制造源码 finding。该局部保护仍不证明所有包管理器布局已识别。 + 先新增真实 CLI 反例:项目本地 ESLint 10 包、入口和配置可见时,旧命令仍启动 WASM,断言因出现 `syntax_precheck` 而失败。接线后该用例通过。下一条端到端用例用受控 Node 命令与本地 ESLint 包建立真实子进程调用,先因 `local_coherent=false` 失败;自动原生调度接线后原生 `no-debugger` 发现进入报告,私有原生消息未泄漏。随后让版本输出从 `10.0.0` 变为 `10.0.1`,确认返回 `eslint_version_mismatch`,不执行 WASM 来洗白失败。缺 Node 的定向断言先因旧 `eslint_execution_context_missing` 失败;新诊断及初始化工作区的两轮稳定任务、具体 `next_action` 已通过。另一个反例保留相同声明和配置但不安装本地包,仍允许候选初检,且 `native=not_run`、`delivery_decision=not_evaluated`。 -本次定向命令:`CARGO_NET_OFFLINE=true cargo test --locked -p codeguard-cli --features wasm-precheck --test typescript_syntax_fallback_candidate --test eslint_lint_cli --test grammar_status_cli`,最终 18 项通过、4 项要求显式真实 Node/ESLint 的用例忽略。前一增量为 17 项通过,不能以旧计数证明稳定环境任务。本机有 Node 24.18.0,但 PATH 上没有 `eslint` 命令,本轮未取得真实 ESLint 10 的原生运行证据。CLI 不安装或修改 `node_modules`,但自动原生路径会按用户的 `lint` 请求执行项目本地 ESLint 及其原配置中的 JavaScript;因此这仍是受控检查执行,不是只读发现命令。 +本次定向命令:`CARGO_NET_OFFLINE=true cargo test --locked -p codeguard-cli --features wasm-precheck --test typescript_syntax_fallback_candidate --test eslint_lint_cli --test grammar_status_cli`,19 项通过、4 项要求显式真实 Node/ESLint 的用例忽略。本机有 Node 24.18.0,但 PATH 上没有 `eslint` 命令,本轮未取得真实 ESLint 10 的原生运行证据。CLI 不安装或修改 `node_modules`,但自动原生路径会按用户的 `lint` 请求执行项目本地 ESLint 及其原配置中的 JavaScript;因此这仍是受控检查执行,不是只读发现命令。 仍缺:项目自定义脚本与参数解析、动态配置/插件闭包的完整性确认、pnpm/Yarn/多模块布局、真实 ESLint 10 运行验收、跨平台与宿主反馈。当前固定选择 `--no-config-lookup --config` 的显式原生命令,未证明它与项目脚本所有行为等价。14.5/14.6 均不勾选。