diff --git a/README.md b/README.md index a3cd8cb5..b99dc463 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` now has narrow TypeScript and Java single-file candidate paths: without explicit native context, they return versioned [TypeScript](schemas/eslint-local-feedback-v0.3.schema.json) or [Java](schemas/java-syntax-precheck-feedback-v0.1.schema.json) feedback with suspected parser recovery positions and native `not_run`. Both remain incomplete because the bundled grammar has no accepted language/dialect range. Default and published binaries keep their previous native-context behavior. [TypeScript acceptance](tests/acceptance/typescript-syntax-fallback-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. 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). ```bash codeguard lint java . diff --git a/README.zh-CN.md b/README.zh-CN.md index d116c971..c41d803d 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 兜底或自动对话交付。** 保留已有命令作为统一入口;当前仍须满足各原生适配器的参数和前置条件: -可选源码构建的单文件候选路径已存在:`codeguard lint java Foo.java` 与 `codeguard lint typescript foo.ts` 在完全缺少显式原生上下文时分别输出 [Java](schemas/java-syntax-precheck-feedback-v0.1.schema.json) 与 [TypeScript](schemas/eslint-local-feedback-v0.3.schema.json) 的版本化初检反馈。疑似恢复位置仍需原生确认;无恢复节点也因 grammar 版本/方言未验收保持 incomplete。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md) 与 [TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)。 +可选源码构建已有 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)。 ```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 118f8337..a4112687 100644 --- a/crates/codeguard-cli/src/eslint_lint_command.rs +++ b/crates/codeguard-cli/src/eslint_lint_command.rs @@ -43,21 +43,38 @@ pub fn run(args: &[String]) -> ExitCode { } #[cfg(feature = "wasm-precheck")] if crate::typescript_syntax_precheck::eligible(&args) { - if crate::eslint_native_first_candidate::observed_candidate(&args.source) { - 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 路径、ESLint 版本与原配置并执行原生检查。候选尚未通过探测,不能认定 lint 已完成" - ); - } + 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 { + 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 + }; 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 = crate::typescript_syntax_precheck::observe(&args, deadline); if args.json { diff --git a/crates/codeguard-cli/src/eslint_native_first_candidate.rs b/crates/codeguard-cli/src/eslint_native_first_candidate.rs index 8031833b..991ecc1e 100644 --- a/crates/codeguard-cli/src/eslint_native_first_candidate.rs +++ b/crates/codeguard-cli/src/eslint_native_first_candidate.rs @@ -3,47 +3,56 @@ use codeguard_adapters::{EslintConfigState, inspect_eslint_local_candidate}; use codeguard_core::ObservedPathKind; use codeguard_runtime::read_bounded_regular_file; -use std::path::Path; +use std::{ + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, +}; -/// 识别源码最近的项目根是否具备可进一步原生探测的本地 ESLint 与配置。 -/// 参数为普通源码路径;返回真仅表示应先确认原生调用,不证明工具可执行或规则有效。 -pub(crate) fn observed_candidate(source: &Path) -> bool { - let Ok(source) = source.canonicalize() else { - return false; - }; - let Some(parent) = source.parent() else { - return false; - }; +/// 已观察到的项目本地 ESLint 10 上下文,尚不代表原生执行成功。 +pub(crate) struct LocalEslintCandidate { + /// 项目构建根,同时是原生检查的工作目录。 + pub(crate) root: PathBuf, + /// 本地 ESLint JS 入口。 + pub(crate) entry: PathBuf, + /// 唯一的普通 flat config。 + pub(crate) config: PathBuf, + /// 本地包清单中观察到的具体版本,须由原生版本探针复核。 + pub(crate) version: String, +} + +/// 识别源码最近项目根的本地 ESLint 包、入口和单一 flat config。 +/// 参数为普通源码路径;返回候选仅表示可以尝试原生版本探测,不证明规则有效。 +pub(crate) fn observed_candidate(source: &Path) -> Option { + let source = source.canonicalize().ok()?; + let parent = source.parent()?; for root in parent.ancestors() { let manifest = root.join("package.json"); if !regular_file(&manifest) { continue; } - let Ok(project_manifest) = read_bounded_regular_file(&manifest, 256 * 1024) else { - return false; - }; + let project_manifest = read_bounded_regular_file(&manifest, 256 * 1024).ok()?; let local_root = root.join("node_modules"); let package_root = local_root.join("eslint"); if !regular_directory(&local_root) || !regular_directory(&package_root) { - return false; + return None; } let local_manifest = package_root.join("package.json"); let entry = package_root.join("bin/eslint.js"); if !regular_file(&local_manifest) || !regular_file(&entry) { - return false; + return None; } - let Ok(local_manifest) = read_bounded_regular_file(&local_manifest, 256 * 1024) else { - return false; - }; - // 仅把 ESLint 10 可直接选取的普通 flat config 作为候选;旧配置和 TS loader 须另行确认。 - let config_count = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"] + 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() - .filter(|name| { - let path = root.join(name); - regular_file(&path) && read_bounded_regular_file(&path, 1024 * 1024).is_ok() - }); - if config_count.count() != 1 { - return false; + .map(|name| root.join(name)) + .filter(|path| std::fs::symlink_metadata(path).is_ok()) + .collect(); + let [config] = configs.as_slice() else { + return None; + }; + if !regular_file(config) || read_bounded_regular_file(config, 1024 * 1024).is_err() { + return None; } let candidate = inspect_eslint_local_candidate( Some(&project_manifest), @@ -51,9 +60,41 @@ pub(crate) fn observed_candidate(source: &Path) -> bool { Some(ObservedPathKind::File), EslintConfigState::Observed, ); - return candidate.state == "local_candidate_requires_native_probe"; + if candidate.state != "local_candidate_requires_native_probe" { + return None; + } + return Some(LocalEslintCandidate { + root: root.to_path_buf(), + entry, + config: config.clone(), + version: candidate.observed_version?, + }); + } + None +} + +/// 从当前进程 PATH 选择普通可执行 Node 文件;缺失只表示当前调用未解析到运行时。 +/// 返回路径会被原生探针按字节冻结,不能据此宣称项目工具不存在或检查完成。 +pub(crate) fn node_on_path() -> Option { + for directory in std::env::split_paths(&std::env::var_os("PATH")?) { + if !directory.is_absolute() { + continue; + } + let node = directory.join("node"); + let Ok(node) = node.canonicalize() else { + continue; + }; + let Ok(metadata) = std::fs::symlink_metadata(&node) else { + continue; + }; + if metadata.is_file() + && metadata.permissions().mode() & 0o111 != 0 + && read_bounded_regular_file(&node, 128 * 1024 * 1024).is_ok() + { + return Some(node); + } } - false + None } fn regular_file(path: &Path) -> bool { diff --git a/crates/codeguard-cli/src/lib.rs b/crates/codeguard-cli/src/lib.rs index 31f21055..2cd146a7 100644 --- a/crates/codeguard-cli/src/lib.rs +++ b/crates/codeguard-cli/src/lib.rs @@ -18,7 +18,7 @@ mod eslint_effective_settings; mod eslint_lint_arguments; #[cfg(unix)] pub mod eslint_lint_command; -#[cfg(feature = "wasm-precheck")] +#[cfg(all(feature = "wasm-precheck", unix))] mod eslint_native_first_candidate; mod eslint_preparation; pub mod eslint_probe; diff --git a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs index 1c1857b8..1de4dbfb 100644 --- a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs +++ b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs @@ -112,8 +112,19 @@ fn observed_project_local_eslint_candidate_is_not_treated_as_missing_native_lint ) .unwrap(); - let (status, report) = project.lint("app.ts"); - assert_eq!(status, 3); + let output = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "lint", + "typescript", + project.0.join("app.ts").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_execution_context_missing"); assert!(report.get("syntax_precheck").is_none()); assert!( @@ -125,6 +136,89 @@ fn observed_project_local_eslint_candidate_is_not_treated_as_missing_native_lint assert_eq!(report["delivery_decision"], "not_evaluated"); } +#[test] +fn available_project_local_eslint_runs_before_wasm_and_preserves_native_finding() { + use std::os::unix::fs::PermissionsExt; + + let project = Project::new(); + let source = project.0.join("app.ts"); + fs::write(&source, "debugger;\n").unwrap(); + fs::write( + project.0.join("package.json"), + r#"{"devDependencies":{"eslint":"10.0.0"}}"#, + ) + .unwrap(); + fs::write( + project.0.join("eslint.config.cjs"), + "module.exports = [];\n", + ) + .unwrap(); + fs::create_dir_all(project.0.join("node_modules/eslint/bin")).unwrap(); + fs::write( + project.0.join("node_modules/eslint/package.json"), + r#"{"name":"eslint","version":"10.0.0","bin":{"eslint":"bin/eslint.js"}}"#, + ) + .unwrap(); + fs::write( + project.0.join("node_modules/eslint/bin/eslint.js"), + "local entry fixture\n", + ) + .unwrap(); + let native = serde_json::json!([{ + "filePath": source.canonicalize().unwrap(), + "messages": [{"ruleId":"no-debugger","severity":2,"message":"private native text","line":1,"column":1}], + "suppressedMessages":[],"errorCount":1,"warningCount":0,"fatalErrorCount":0, + "fixableErrorCount":0,"fixableWarningCount":0 + }]); + let node = project.0.join("node"); + fs::write( + &node, + format!( + "#!/bin/sh\nfor arg in \"$@\"; do if [ \"$arg\" = --version ]; then printf 'v10.0.0\\n'; exit 0; fi; done\nwhile [ \"$#\" -gt 0 ]; do if [ \"$1\" = --output-file ]; then shift; report=$1; fi; shift; done\nprintf '%s' '{}' > \"$report\"\nexit 1\n", + native + ), + ) + .unwrap(); + fs::set_permissions(&node, fs::Permissions::from_mode(0o700)).unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "lint", + "typescript", + source.to_str().unwrap(), + "--format", + "json", + ]) + .env("PATH", &project.0) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(3)); + let report: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(report["local_coherent"], true, "{report}"); + assert_eq!(report["findings"][0]["rule_id"], "no-debugger"); + assert!(report.get("syntax_precheck").is_none()); + assert_eq!(report["delivery_decision"], "not_evaluated"); + assert!(!String::from_utf8_lossy(&output.stdout).contains("private native text")); + + fs::write(&node, "#!/bin/sh\nprintf 'v10.0.1\\n'\n").unwrap(); + let mismatch = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "lint", + "typescript", + source.to_str().unwrap(), + "--format", + "json", + ]) + .env("PATH", &project.0) + .output() + .unwrap(); + assert_eq!(mismatch.status.code(), Some(3)); + let mismatch: Value = serde_json::from_slice(&mismatch.stdout).unwrap(); + assert_eq!(mismatch["reason"], "eslint_version_mismatch"); + assert_eq!(mismatch["local_coherent"], false); + assert!(mismatch.get("syntax_precheck").is_none()); +} + #[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 8b1b0732..10bb611d 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 ` candidate observations when no explicit native execution context is provided. They use the same commands and return [TypeScript feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) or [Java feedback 0.1.0](../schemas/java-syntax-precheck-feedback-v0.1.schema.json), both with exit 3. The Java path covers only an ordinary single `.java` file; partial native context, Javadoc/Checkstyle selections and symlinks retain their existing paths. Neither candidate promotes an unvalidated grammar to native lint success. Default/published binaries retain the existing native-context behavior. +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). `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 a646408e..04bc9a07 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 <显式单文件>` 或 `lint java <显式单文件>` 完全缺少原生执行上下文时分别输出 [TypeScript 0.3.0 候选反馈](../schemas/eslint-local-feedback-v0.3.schema.json)与 [Java 0.1.0 候选反馈](../schemas/java-syntax-precheck-feedback-v0.1.schema.json)。Java 路径仅选择普通 `.java` 单文件,部分原生参数、Javadoc/Checkstyle 和符号链接保持既有原生路径。两者均保留原生 `not_run`、总体 incomplete 与退出 3;默认和已发布二进制仍走既有原生上下文路径。见[TypeScript 局部验收](../tests/acceptance/typescript-syntax-fallback-candidate.md)、[Java 局部验收](../tests/acceptance/java-syntax-fallback-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 则返回准备缺口。无可见本地入口时才输出 [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与闭环承诺均为目标契约。 ## 1. 命令体系与协作路线 diff --git a/docs/Codeguard-Technical-Design.md b/docs/Codeguard-Technical-Design.md index 4c854190..e3bdbe93 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 emits [ESLint feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) for `.ts/.mts/.cts`, and [0.4.0](../schemas/eslint-local-feedback-v0.4.schema.json) for `.tsx`, when no native execution context was supplied. TSX uses its own pinned grammar, avoiding a TypeScript-parser false positive for valid JSX. Both preserve `native=not_run` and `delivery=not_evaluated`, map parser recoveries to suspected Unicode positions, and remain incomplete even with zero recoveries. A partial native context or symlink does not trigger this candidate path; configured native execution stays on the original route. This is one local entry-point slice, not per-module native-first orchestration or automatic host feedback. +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). ### 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 a783b433..e5edb93b 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 在 `.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),避免普通 TypeScript grammar 把有效 JSX 标为疑似异常。两者保留 `native=not_run`、`delivery=not_evaluated`,恢复节点只作为 Unicode 位置的疑似观察;即使零恢复节点也保持总体未完成。部分原生上下文或符号链接不触发候选路径,已配置的原生执行继续走原路径。这只是一个局部统一入口,不代表逐模块原生优先调度或宿主自动对话已完成。 +可选特性构建的 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)。 ### 5.4 Grammar 引入与运行生命周期——目标 diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index 7dbe486a..dfd28239 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -1032,8 +1032,8 @@ 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)。显式工具、受控原生版本探针、原生优先调度、更多构建器和真实多平台验收尚缺,不勾选。 -- [ ] 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 候选与配置可见时,单文件 TypeScript 路径现在先返回待原生探测而不抢先启动 WASM;只声明/配置但无本地入口仍可初检,见 [本地原生优先局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。这不证明本地原生工具已执行;可信 Node/参数自动选择、多模块调度、真实原生/初检混合保留仍缺,不勾选。 +- [ ] 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.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) 局部验收。正式四类报告、多模块范围与宿主渲染仍缺,不勾选。 diff --git a/tests/acceptance/native-first-eslint-candidate.md b/tests/acceptance/native-first-eslint-candidate.md index 1d0a3128..b7774775 100644 --- a/tests/acceptance/native-first-eslint-candidate.md +++ b/tests/acceptance/native-first-eslint-candidate.md @@ -1,9 +1,11 @@ # TypeScript 本地 ESLint 候选优先:局部验收(2026-09-29) -对应 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` 时,命令不启动 WASM worker,返回未完成的原生准备反馈,并提示先核对 Node 路径、版本与原配置。旧 `.eslintrc`、TypeScript 配置及多个 flat config 不作为可直接选取的候选。候选的存在不等于原生可运行;本增量不自动执行本地入口,也不授予交付通过。 +对应 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 不作为可直接选取的候选。 -先新增真实 CLI 反例:项目本地 ESLint 10 包、入口和配置可见时,旧命令仍启动 WASM,断言因出现 `syntax_precheck` 而失败。接线后该用例通过。另一个反例保留相同声明和配置但不安装本地包,仍允许候选初检,且 `native=not_run`、`delivery_decision=not_evaluated`。`eslint_lint_cli` 现有显式原生路径与 `grammar_status_cli` 一并回归。 +若能解析到 Node,CLI 使用本地包清单中的具体版本、项目入口/配置和构建根调用现有受控原生探针;Node、入口、配置和源码均由探针按字节核对,版本输出与报告范围必须吻合。探针得到的发现仅是局部原生观察,仍为 `delivery_decision=not_evaluated`。若当前 `PATH` 无可用 Node,仅返回原生准备动作,不把 PATH 缺失当作项目工具未安装,也不启动 WASM 抢跑。项目只声明 ESLint、没有本地入口时仍可提供候选语法初检。 -本次定向测试:`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`,共 16 通过、4 项需要显式真实 Node/ESLint 的用例忽略。候选探测不执行项目 JavaScript、不修改 `node_modules`,不把“未给显式参数”当作“原生工具不存在”。 +先新增真实 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`。 -仍缺:可信 Node 路径与原调用参数解析、受控版本探针、动态配置有效性确认、项目本地原生自动执行、多模块原生优先调度及真实宿主反馈。因此 14.5/14.6 均不勾选。 +本次定向命令:`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;因此这仍是受控检查执行,不是只读发现命令。 + +仍缺:项目自定义脚本与参数解析、动态配置/插件闭包的完整性确认、pnpm/Yarn/多模块布局、真实 ESLint 10 运行验收、跨平台与宿主反馈。当前固定选择 `--no-config-lookup --config` 的显式原生命令,未证明它与项目脚本所有行为等价。14.5/14.6 均不勾选。