From 35a75bef6f5528b1a27e2e0aa30d68a78fd91d1b Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:05:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=86=E7=BC=BA=E5=A4=B1=20Node=20?= =?UTF-8?q?=E5=BD=92=E5=85=A5=E7=A8=B3=E5=AE=9A=E7=8E=AF=E5=A2=83=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codeguard-cli/src/eslint_lint_command.rs | 30 +++++--- .../codeguard-cli/src/eslint_preparation.rs | 8 ++- .../typescript_syntax_fallback_candidate.rs | 71 ++++++++++++++++++- .../introduce-rust-codeguard-cli/tasks.md | 2 +- .../native-first-eslint-candidate.md | 6 +- 5 files changed, 102 insertions(+), 15 deletions(-) diff --git a/crates/codeguard-cli/src/eslint_lint_command.rs b/crates/codeguard-cli/src/eslint_lint_command.rs index a4112687..3839d452 100644 --- a/crates/codeguard-cli/src/eslint_lint_command.rs +++ b/crates/codeguard-cli/src/eslint_lint_command.rs @@ -55,15 +55,11 @@ pub fn run(args: &[String]) -> ExitCode { native_args.node = Some(node); observe_with_preparation(&native_args, deadline) } else { - let mut report = observe_with_preparation(&args, deadline); - if report["reason"] == "eslint_execution_context_missing" - && report["workbench"]["next"]["repair_brief"]["step"].is_null() - { - report["next_action"] = json!( - "项目本地 ESLint 候选和配置已发现;当前未解析到 Node,请提供 Node 路径并执行原生检查。候选尚未通过探测,不能认定 lint 已完成" - ); - } - report + observe_with_preparation_reason( + &args, + deadline, + Some("eslint_node_runtime_unresolved"), + ) }; if args.json { println!("{report}"); @@ -97,7 +93,22 @@ pub fn run(args: &[String]) -> ExitCode { }) } pub(crate) fn observe_with_preparation(args: &EslintLintArguments, deadline: Instant) -> Value { + observe_with_preparation_reason(args, deadline, None) +} +fn observe_with_preparation_reason( + args: &EslintLintArguments, + deadline: Instant, + missing_context_reason: Option<&str>, +) -> Value { let mut report = observe(args, deadline); + if report["reason"] == "eslint_execution_context_missing" + && missing_context_reason == Some("eslint_node_runtime_unresolved") + { + report["reason"] = json!("eslint_node_runtime_unresolved"); + report["next_action"] = json!( + "项目本地 ESLint 候选与原配置已发现;当前未解析到 Node。核对项目运行时并提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码" + ); + } // 早期上下文失败也进入环境待办;取消、外部目标或不支持范围不能制造源码任务。 if let (Some(workspace), Some(reason)) = ( &args.workspace, @@ -266,6 +277,7 @@ fn observe_captured( 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_scope_requires_explicit_file"=>"当前入口需显式单文件;项目目录与完整源集调度尚未接通,不将目录标为已检查", "request_cancelled"|"request_deadline_exceeded"=>"重新安排原工具检查预算;中断不要求修改源码", _=>"核对原配置、parser/插件与本轮规则;按原生发现修复后用同一入口和配置复检,抑制及未知覆盖需核查,任务不能自行关闭", diff --git a/crates/codeguard-cli/src/eslint_preparation.rs b/crates/codeguard-cli/src/eslint_preparation.rs index ff3912ec..95d80475 100644 --- a/crates/codeguard-cli/src/eslint_preparation.rs +++ b/crates/codeguard-cli/src/eslint_preparation.rs @@ -142,6 +142,7 @@ pub(crate) fn valid_reason(reason: &str) -> bool { matches!( reason, "eslint_execution_context_missing" + | "eslint_node_runtime_unresolved" | "eslint_input_unavailable" | "eslint_private_workspace_unavailable" | "request_deadline_exceeded" @@ -292,5 +293,10 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value { { return fallback; } - json!({"disposition":"actionable","step":"核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码","diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha}) + let step = if report["diagnostic_reason"] == "eslint_node_runtime_unresolved" { + "项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码" + } else { + "核对最新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 1de4dbfb..ff023306 100644 --- a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs +++ b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs @@ -125,7 +125,7 @@ fn observed_project_local_eslint_candidate_is_not_treated_as_missing_native_lint .unwrap(); assert_eq!(output.status.code(), Some(3)); let report: Value = serde_json::from_slice(&output.stdout).unwrap(); - assert_eq!(report["reason"], "eslint_execution_context_missing"); + assert_eq!(report["reason"], "eslint_node_runtime_unresolved"); assert!(report.get("syntax_precheck").is_none()); assert!( report["next_action"] @@ -219,6 +219,75 @@ fn available_project_local_eslint_runs_before_wasm_and_preserves_native_finding( assert!(mismatch.get("syntax_precheck").is_none()); } +#[test] +fn unresolved_node_updates_one_stable_environment_task_with_specific_guidance() { + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + let source = root.join("app.ts"); + fs::write(&source, "const value: number = 1;\n").unwrap(); + fs::write( + root.join("package.json"), + r#"{"devDependencies":{"eslint":"10.0.0"}}"#, + ) + .unwrap(); + fs::write(root.join("eslint.config.cjs"), "module.exports = [];\n").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(); + fs::write(root.join("node_modules/eslint/bin/eslint.js"), "fixture\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 mut task_id = None; + for round in 0..2 { + let output = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "lint", + "typescript", + source.to_str().unwrap(), + "--workspace", + root.to_str().unwrap(), + "--format", + "json", + ]) + .env("PATH", "") + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(3)); + let report: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(report["reason"], "eslint_node_runtime_unresolved"); + assert_eq!(report["workbench_status"], "synced_partial", "{report}"); + assert_eq!(report["workbench"]["new_blockers"], 1 - round); + assert!(report["next_action"].as_str().unwrap().contains("Node")); + assert!( + report["next_action"] + .as_str() + .unwrap() + .contains("不重复安装 ESLint") + ); + let current = report["workbench"]["next"]["repair_brief"]["task_id"] + .as_str() + .unwrap() + .to_owned(); + if let Some(previous) = &task_id { + assert_eq!(¤t, previous); + } + task_id = Some(current); + } +} + #[test] fn configured_but_uninstalled_eslint_still_allows_candidate_precheck() { let project = Project::new(); diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index dfd28239..9a846892 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -1037,7 +1037,7 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30 - [ ] 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。 +- [ ] 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.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 b7774775..3c2ab1ed 100644 --- a/tests/acceptance/native-first-eslint-candidate.md +++ b/tests/acceptance/native-first-eslint-candidate.md @@ -2,10 +2,10 @@ 对应 OpenSpec 14.6。启用 `wasm-precheck` 的单文件 `lint typescript` 在缺少显式原生参数时,先只读核对源码最近的 `package.json`、普通 `node_modules/eslint/package.json` 与 `bin/eslint.js`,以及唯一的普通 `eslint.config.js/mjs/cjs`。只有适配器将其分类为 `local_candidate_requires_native_probe` 时,才尝试从当前 `PATH` 选择普通可执行 Node 文件;候选本身不能证明原生已运行。旧 `.eslintrc`、TypeScript 配置及多个 flat config 不作为可直接选取的候选。 -若能解析到 Node,CLI 使用本地包清单中的具体版本、项目入口/配置和构建根调用现有受控原生探针;Node、入口、配置和源码均由探针按字节核对,版本输出与报告范围必须吻合。探针得到的发现仅是局部原生观察,仍为 `delivery_decision=not_evaluated`。若当前 `PATH` 无可用 Node,仅返回原生准备动作,不把 PATH 缺失当作项目工具未安装,也不启动 WASM 抢跑。项目只声明 ESLint、没有本地入口时仍可提供候选语法初检。 +若能解析到 Node,CLI 使用本地包清单中的具体版本、项目入口/配置和构建根调用现有受控原生探针;Node、入口、配置和源码均由探针按字节核对,版本输出与报告范围必须吻合。探针得到的发现仅是局部原生观察,仍为 `delivery_decision=not_evaluated`。若当前 `PATH` 无可用 Node,反馈给出 `eslint_node_runtime_unresolved`,不把 PATH 缺失当作项目工具未安装,也不启动 WASM 抢跑。已初始化的工作区把它同步为原有 ESLint 环境任务;两轮扫描复用同一任务,`next` 明确要求提供受控 Node 路径,避免重复安装 ESLint 或修改无依据源码。项目只声明 ESLint、没有本地入口时仍可提供候选语法初检。 -先新增真实 CLI 反例:项目本地 ESLint 10 包、入口和配置可见时,旧命令仍启动 WASM,断言因出现 `syntax_precheck` 而失败。接线后该用例通过。下一条端到端用例用受控 Node 命令与本地 ESLint 包建立真实子进程调用,先因 `local_coherent=false` 失败;自动原生调度接线后原生 `no-debugger` 发现进入报告,私有原生消息未泄漏。随后让版本输出从 `10.0.0` 变为 `10.0.1`,确认返回 `eslint_version_mismatch`,不执行 WASM 来洗白失败。另一个反例保留相同声明和配置但不安装本地包,仍允许候选初检,且 `native=not_run`、`delivery_decision=not_evaluated`。 +先新增真实 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`,17 项通过,4 项要求显式真实 Node/ESLint 的用例仍忽略。先前只读候选阶段为 16 项通过,不能把该旧数字作为本次自动原生阶段的验收。当前机器有 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`,最终 18 项通过、4 项要求显式真实 Node/ESLint 的用例忽略。前一增量为 17 项通过,不能以旧计数证明稳定环境任务。本机有 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 均不勾选。