Skip to content

Commit 02a4e6f

Browse files
authored
Merge pull request #12 from full-stack-plugins/feat/rust-hook-prompt-guidance
fix: create stable Node runtime blocker for local ESLint
2 parents 169ed45 + 1b4c6c2 commit 02a4e6f

5 files changed

Lines changed: 102 additions & 15 deletions

File tree

‎crates/codeguard-cli/src/eslint_lint_command.rs‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,11 @@ pub fn run(args: &[String]) -> ExitCode {
5555
native_args.node = Some(node);
5656
observe_with_preparation(&native_args, deadline)
5757
} else {
58-
let mut report = observe_with_preparation(&args, deadline);
59-
if report["reason"] == "eslint_execution_context_missing"
60-
&& report["workbench"]["next"]["repair_brief"]["step"].is_null()
61-
{
62-
report["next_action"] = json!(
63-
"项目本地 ESLint 候选和配置已发现;当前未解析到 Node,请提供 Node 路径并执行原生检查。候选尚未通过探测,不能认定 lint 已完成"
64-
);
65-
}
66-
report
58+
observe_with_preparation_reason(
59+
&args,
60+
deadline,
61+
Some("eslint_node_runtime_unresolved"),
62+
)
6763
};
6864
if args.json {
6965
println!("{report}");
@@ -97,7 +93,22 @@ pub fn run(args: &[String]) -> ExitCode {
9793
})
9894
}
9995
pub(crate) fn observe_with_preparation(args: &EslintLintArguments, deadline: Instant) -> Value {
96+
observe_with_preparation_reason(args, deadline, None)
97+
}
98+
fn observe_with_preparation_reason(
99+
args: &EslintLintArguments,
100+
deadline: Instant,
101+
missing_context_reason: Option<&str>,
102+
) -> Value {
100103
let mut report = observe(args, deadline);
104+
if report["reason"] == "eslint_execution_context_missing"
105+
&& missing_context_reason == Some("eslint_node_runtime_unresolved")
106+
{
107+
report["reason"] = json!("eslint_node_runtime_unresolved");
108+
report["next_action"] = json!(
109+
"项目本地 ESLint 候选与原配置已发现;当前未解析到 Node。核对项目运行时并提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码"
110+
);
111+
}
101112
// 早期上下文失败也进入环境待办;取消、外部目标或不支持范围不能制造源码任务。
102113
if let (Some(workspace), Some(reason)) = (
103114
&args.workspace,
@@ -266,6 +277,7 @@ fn observe_captured(
266277
fn feedback(reason: &str) -> Value {
267278
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 {
268279
"eslint_execution_context_missing"=>"提供显式 Node、原 ESLint JS 入口、具体版本、原工作目录与项目原 flat config 后复检;不安装或替换规则",
280+
"eslint_node_runtime_unresolved"=>"项目本地 ESLint 候选已发现;提供受控 Node 路径后原生复检,不重复安装 ESLint 或修改无依据源码",
269281
"eslint_scope_requires_explicit_file"=>"当前入口需显式单文件;项目目录与完整源集调度尚未接通,不将目录标为已检查",
270282
"request_cancelled"|"request_deadline_exceeded"=>"重新安排原工具检查预算;中断不要求修改源码",
271283
_=>"核对原配置、parser/插件与本轮规则;按原生发现修复后用同一入口和配置复检,抑制及未知覆盖需核查,任务不能自行关闭",

‎crates/codeguard-cli/src/eslint_preparation.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ pub(crate) fn valid_reason(reason: &str) -> bool {
142142
matches!(
143143
reason,
144144
"eslint_execution_context_missing"
145+
| "eslint_node_runtime_unresolved"
145146
| "eslint_input_unavailable"
146147
| "eslint_private_workspace_unavailable"
147148
| "request_deadline_exceeded"
@@ -292,5 +293,10 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value {
292293
{
293294
return fallback;
294295
}
295-
json!({"disposition":"actionable","step":"核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码","diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha})
296+
let step = if report["diagnostic_reason"] == "eslint_node_runtime_unresolved" {
297+
"项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码"
298+
} else {
299+
"核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码"
300+
};
301+
json!({"disposition":"actionable","step":step,"diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha})
296302
}

‎crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ fn observed_project_local_eslint_candidate_is_not_treated_as_missing_native_lint
125125
.unwrap();
126126
assert_eq!(output.status.code(), Some(3));
127127
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
128-
assert_eq!(report["reason"], "eslint_execution_context_missing");
128+
assert_eq!(report["reason"], "eslint_node_runtime_unresolved");
129129
assert!(report.get("syntax_precheck").is_none());
130130
assert!(
131131
report["next_action"]
@@ -219,6 +219,75 @@ fn available_project_local_eslint_runs_before_wasm_and_preserves_native_finding(
219219
assert!(mismatch.get("syntax_precheck").is_none());
220220
}
221221

222+
#[test]
223+
fn unresolved_node_updates_one_stable_environment_task_with_specific_guidance() {
224+
let project = Project::new();
225+
let root = project.0.canonicalize().unwrap();
226+
let source = root.join("app.ts");
227+
fs::write(&source, "const value: number = 1;\n").unwrap();
228+
fs::write(
229+
root.join("package.json"),
230+
r#"{"devDependencies":{"eslint":"10.0.0"}}"#,
231+
)
232+
.unwrap();
233+
fs::write(root.join("eslint.config.cjs"), "module.exports = [];\n").unwrap();
234+
fs::create_dir_all(root.join("node_modules/eslint/bin")).unwrap();
235+
fs::write(
236+
root.join("node_modules/eslint/package.json"),
237+
r#"{"name":"eslint","version":"10.0.0","bin":{"eslint":"bin/eslint.js"}}"#,
238+
)
239+
.unwrap();
240+
fs::write(root.join("node_modules/eslint/bin/eslint.js"), "fixture\n").unwrap();
241+
let initialized = Command::new(env!("CARGO_BIN_EXE_codeguard"))
242+
.args([
243+
"init",
244+
root.to_str().unwrap(),
245+
"--apply",
246+
"--format",
247+
"json",
248+
])
249+
.output()
250+
.unwrap();
251+
assert_eq!(initialized.status.code(), Some(3));
252+
253+
let mut task_id = None;
254+
for round in 0..2 {
255+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
256+
.args([
257+
"lint",
258+
"typescript",
259+
source.to_str().unwrap(),
260+
"--workspace",
261+
root.to_str().unwrap(),
262+
"--format",
263+
"json",
264+
])
265+
.env("PATH", "")
266+
.output()
267+
.unwrap();
268+
assert_eq!(output.status.code(), Some(3));
269+
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
270+
assert_eq!(report["reason"], "eslint_node_runtime_unresolved");
271+
assert_eq!(report["workbench_status"], "synced_partial", "{report}");
272+
assert_eq!(report["workbench"]["new_blockers"], 1 - round);
273+
assert!(report["next_action"].as_str().unwrap().contains("Node"));
274+
assert!(
275+
report["next_action"]
276+
.as_str()
277+
.unwrap()
278+
.contains("不重复安装 ESLint")
279+
);
280+
let current = report["workbench"]["next"]["repair_brief"]["task_id"]
281+
.as_str()
282+
.unwrap()
283+
.to_owned();
284+
if let Some(previous) = &task_id {
285+
assert_eq!(&current, previous);
286+
}
287+
task_id = Some(current);
288+
}
289+
}
290+
222291
#[test]
223292
fn configured_but_uninstalled_eslint_still_allows_candidate_precheck() {
224293
let project = Project::new();

‎openspec/changes/introduce-rust-codeguard-cli/tasks.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,7 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30
10371037
- [ ] 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 拒绝与发布兼容仍缺,不勾选。
10381038
- [ ] 14.8 安装建议与必须确认策略。责任:core。规格:SP03。依赖:14.6,14.7。验收:可选原生缺失且完整 clean 仅推荐;疑似或已有必需义务要求恢复原生确认;无适配器给具体决策。进行中:core 已有纯决策函数,区分缺工具、坏配置、执行失败和无适配器;仅非空、完整、已验收且无恢复节点的 clean 才允许可选建议,伪造 clean 状态不能降级。见[局部验收](../../../tests/acceptance/syntax-setup-guidance-candidate.md)。真实工具准备来源、CLI/工作台任务投影及宿主反馈尚未接线,不勾选。
10391039
- [ ] 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) 局部验收。正式四类报告、多模块范围与宿主渲染仍缺,不勾选。
1040-
- [ ] 14.10 稳定任务与下一步。责任:core/CLI。规格:SP08。依赖:14.8,14.9及现有工作台。验收:重复扫描更新同一准备/确认任务,失败尝试有记录,无进展给具体诊断;推荐项不持续占用 next。
1040+
- [ ] 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 疑似观察到确认任务的关联、失败尝试/无进展预算、推荐项非阻断投影和多模块任务尚缺,不勾选。
10411041
- [ ] 14.11 能力匹配复检。责任:core/adapters。规格:SP08。依赖:14.10。验收:同模块/输入/方言的原生语法能力确认才能关闭;仅安装、后续 WASM clean 或格式检查不能关闭;复发重开。
10421042
- [ ] 14.12 精确误报纠错。责任:core。规格:SP09。依赖:14.11及既有白名单提案。验收:绑定 grammar/规则/范围/版本和可信审批,变更失效;原生反证保留历史并进语料,不撤销原生义务。
10431043
- [ ] 14.13 缓存与失效恢复。责任:runtime/core。规格:SP10。依赖:14.7。验收:输入、grammar、runtime、规则和配置变化失效;不缓存不完整为 clean;升级回滚不丢历史。

‎tests/acceptance/native-first-eslint-candidate.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
对应 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 不作为可直接选取的候选。
44

5-
若能解析到 Node,CLI 使用本地包清单中的具体版本、项目入口/配置和构建根调用现有受控原生探针;Node、入口、配置和源码均由探针按字节核对,版本输出与报告范围必须吻合。探针得到的发现仅是局部原生观察,仍为 `delivery_decision=not_evaluated`。若当前 `PATH` 无可用 Node,仅返回原生准备动作,不把 PATH 缺失当作项目工具未安装,也不启动 WASM 抢跑。项目只声明 ESLint、没有本地入口时仍可提供候选语法初检。
5+
若能解析到 Node,CLI 使用本地包清单中的具体版本、项目入口/配置和构建根调用现有受控原生探针;Node、入口、配置和源码均由探针按字节核对,版本输出与报告范围必须吻合。探针得到的发现仅是局部原生观察,仍为 `delivery_decision=not_evaluated`。若当前 `PATH` 无可用 Node,反馈给出 `eslint_node_runtime_unresolved`,不把 PATH 缺失当作项目工具未安装,也不启动 WASM 抢跑。已初始化的工作区把它同步为原有 ESLint 环境任务;两轮扫描复用同一任务,`next` 明确要求提供受控 Node 路径,避免重复安装 ESLint 或修改无依据源码。项目只声明 ESLint、没有本地入口时仍可提供候选语法初检。
66

7-
先新增真实 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`。
7+
先新增真实 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`。
88

9-
本次定向命令:`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;因此这仍是受控检查执行,不是只读发现命令。
9+
本次定向命令:`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;因此这仍是受控检查执行,不是只读发现命令。
1010

1111
仍缺:项目自定义脚本与参数解析、动态配置/插件闭包的完整性确认、pnpm/Yarn/多模块布局、真实 ESLint 10 运行验收、跨平台与宿主反馈。当前固定选择 `--no-config-lookup --config` 的显式原生命令,未证明它与项目脚本所有行为等价。14.5/14.6 均不勾选。

0 commit comments

Comments
 (0)