Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions crates/codeguard-cli/src/eslint_lint_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Comment on lines +58 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deduplicate the Node blocker at module scope

When two TypeScript files in the same project-local ESLint module are scanned without Node on PATH, this new path creates two environment tasks rather than updating one: observe_with_preparation_reason passes the per-file args, and eslint_preparation::prepare includes that source-relative path in the blocker fingerprint. The test only repeats one file, so it misses the task pileup across files; key this runtime blocker to the discovered candidate root/checking obligation instead.

Useful? React with 👍 / 👎.

)
};
if args.json {
println!("{report}");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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/插件与本轮规则;按原生发现修复后用同一入口和配置复检,抑制及未知覆盖需核查,任务不能自行关闭",
Expand Down
8 changes: 7 additions & 1 deletion crates/codeguard-cli/src/eslint_preparation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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!(&current, previous);
}
task_id = Some(current);
}
}

#[test]
fn configured_but_uninstalled_eslint_still_allows_candidate_precheck() {
let project = Project::new();
Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/introduce-rust-codeguard-cli/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;升级回滚不丢历史。
Expand Down
6 changes: 3 additions & 3 deletions tests/acceptance/native-first-eslint-candidate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 均不勾选。
Loading