diff --git a/README.md b/README.md index d8840292..e499a6f4 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ Generic `dependencies`, generic `security`, arbitrary category/language combinat 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). -With an initialized `--workspace`, that TypeScript/TSX fallback now returns a real, stable native-confirmation task ID after sync; repeated WASM scans never close it. The individual suspected positions and a capability-matched native closure still need implementation. [Workbench acceptance](tests/acceptance/typescript-syntax-confirmation-task.md). +With an initialized `--workspace`, that TypeScript/TSX fallback now returns a real, stable native-confirmation task ID after sync; repeated WASM scans never close it. A versioned local report now preserves bounded suspected positions with source and grammar identities; capability-matched native closure still needs implementation. [Workbench acceptance](tests/acceptance/typescript-syntax-confirmation-task.md). ```bash codeguard lint java . diff --git a/README.zh-CN.md b/README.zh-CN.md index dfbdd6ba..ef56fcbb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -182,7 +182,7 @@ Python 编辑快反馈可执行 `codeguard lint python . --file src/changed.py - 可选源码构建已有 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)。 -已初始化工作区使用 `--workspace` 时,TypeScript/TSX 候选回退在同步后返回真实且稳定的原生确认任务 ID;反复 WASM 扫描不会自行关闭任务。逐个疑似位置的持久关联和能力匹配的原生关闭仍待实现,见[局部验收](tests/acceptance/typescript-syntax-confirmation-task.md)。 +已初始化工作区使用 `--workspace` 时,TypeScript/TSX 候选回退在同步后返回真实且稳定的原生确认任务 ID;反复 WASM 扫描不会自行关闭任务。版本化本地报告已按源码与 grammar 身份保存有界疑似位置;能力匹配的原生关闭仍待实现,见[局部验收](tests/acceptance/typescript-syntax-confirmation-task.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 566afa7a..bd7d21f3 100644 --- a/crates/codeguard-cli/src/eslint_lint_command.rs +++ b/crates/codeguard-cli/src/eslint_lint_command.rs @@ -106,6 +106,13 @@ fn connect_syntax_confirmation(args: &EslintLintArguments, report: &mut Value) { "eslint_syntax_precheck_unavailable" }; report["workbench"] = match workspace.canonicalize() { + Ok(root) if root == *workspace && reason == "eslint_syntax_confirmation_needed" => { + crate::eslint_workbench::connect_syntax_preparation( + &root, + args, + &report["syntax_precheck"], + ) + } Ok(root) if root == *workspace => { crate::eslint_workbench::connect_preparation(&root, args, reason) } diff --git a/crates/codeguard-cli/src/eslint_preparation.rs b/crates/codeguard-cli/src/eslint_preparation.rs index c8445aba..e3d41ac8 100644 --- a/crates/codeguard-cli/src/eslint_preparation.rs +++ b/crates/codeguard-cli/src/eslint_preparation.rs @@ -66,6 +66,26 @@ pub(crate) fn prepare( "affected_paths":[relative],"source_sha256":format!("{:x}",Sha256::digest(bytes))}), ) } +#[cfg(unix)] +pub(crate) fn prepare_with_syntax( + root: &Path, + args: &EslintLintArguments, + precheck: &Value, +) -> Result { + let mut report = prepare(root, args, "eslint_syntax_confirmation_needed")?; + let source = read_bounded_regular_file(&args.source, 16 * 1024 * 1024) + .map_err(|_| "eslint_source_unavailable")?; + if report["source_sha256"] != format!("{:x}", Sha256::digest(&source)) { + return Err("syntax_evidence_source_changed"); + } + let scope = report["scope"] + .as_str() + .ok_or("preparation_scope_invalid")?; + let evidence = crate::eslint_syntax_evidence::project(precheck, scope, &source)?; + report["schema_version"] = json!("0.2.0"); + report["syntax_evidence"] = evidence; + Ok(report) +} pub(crate) fn fingerprint(workspace: &str, scope: &str) -> String { let mut hash = Sha256::new(); for part in [ @@ -104,10 +124,20 @@ pub(crate) fn valid_report(report: &Value, workspace: &str) -> bool { return false; }; let fp = fingerprint(workspace, scope); - report - .as_object() - .is_some_and(|m| m.len() == keys.len() && keys.iter().all(|k| m.contains_key(*k))) - && report["schema_version"] == "0.1.0" + let version_valid = match report["schema_version"].as_str() { + Some("0.1.0") => report.as_object().is_some_and(|m| m.len() == keys.len()), + Some("0.2.0") => { + report + .as_object() + .is_some_and(|m| m.len() == keys.len() + 1 && m.contains_key("syntax_evidence")) + && report["diagnostic_reason"] == "eslint_syntax_confirmation_needed" + } + _ => false, + }; + version_valid + && report + .as_object() + .is_some_and(|m| keys.iter().all(|k| m.contains_key(*k))) && report["report_type"] == "eslint_preparation_observation" && report["workspace_binding"] == "bound" && report["workspace_id"] == workspace @@ -296,10 +326,19 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value { return fallback; } let source = root.join(scope); - if source.canonicalize().ok().as_deref() != Some(source.as_path()) - || read_bounded_regular_file(&source, 16 * 1024 * 1024) - .ok() - .is_none_or(|b| report["source_sha256"] != format!("{:x}", Sha256::digest(b))) + if source.canonicalize().ok().as_deref() != Some(source.as_path()) { + return fallback; + } + let Ok(source_bytes) = read_bounded_regular_file(&source, 16 * 1024 * 1024) else { + return fallback; + }; + if report["source_sha256"] != format!("{:x}", Sha256::digest(&source_bytes)) + || (report["schema_version"] == "0.2.0" + && !crate::eslint_syntax_evidence::valid( + &report["syntax_evidence"], + scope, + &source_bytes, + )) { return fallback; } @@ -333,5 +372,12 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value { "核对最新ESLint前置诊断、原配置、parser/插件和运行条件;解析或抑制先复现根因,恢复原检查后复扫,不修改无关源码" } }; - json!({"disposition":"actionable","step":step,"diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha}) + let mut guidance = json!({"disposition":"actionable","step":step,"diagnostic_reason":report["diagnostic_reason"],"run_id":run,"report_sha256":sha}); + if report["schema_version"] == "0.2.0" { + guidance["suspected_positions"] = report["syntax_evidence"]["observations"].clone(); + guidance["source_sha256"] = report["source_sha256"].clone(); + guidance["grammar_sha256"] = report["syntax_evidence"]["grammar_sha256"].clone(); + guidance["evidence_authority"] = json!("local_unverified_suspected"); + } + guidance } diff --git a/crates/codeguard-cli/src/eslint_syntax_evidence.rs b/crates/codeguard-cli/src/eslint_syntax_evidence.rs new file mode 100644 index 00000000..b94e651b --- /dev/null +++ b/crates/codeguard-cli/src/eslint_syntax_evidence.rs @@ -0,0 +1,145 @@ +//! ESLint 原生确认任务中的脱敏 WASM 疑似位置;只具待核实证据权威。 + +use codeguard_adapters::bundled_grammar_candidates; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; + +/// 从当轮候选反馈提取有界坐标,不保存源码片段或语法节点文本。 +/// 参数为初检反馈、工作区相对路径和当前源码字节;返回绑定固定 grammar 的疑似证据。 +pub(crate) fn project(precheck: &Value, scope: &str, source: &[u8]) -> Result { + let observations = precheck["observations"] + .as_array() + .filter(|rows| rows.len() <= 128) + .ok_or("syntax_evidence_observations_invalid")?; + let positions: Vec = observations + .iter() + .map(|row| { + json!({ + "classification":row["classification"], + "kind":row["kind"], + "start_line":row["start_line"], + "start_column":row["start_column"], + "end_line":row["end_line"], + "end_column":row["end_column"], + "group_id":row["group_id"] + }) + }) + .collect(); + let evidence = json!({ + "schema_version":"1.0.0", + "backend":"bundled_tree_sitter_wasm_candidate", + "language":precheck["language"], + "scope":scope, + "source_sha256":precheck["source_sha256"], + "grammar_sha256":precheck["grammar_sha256"], + "grammar_qualified":precheck["grammar_qualified"], + "precheck_reason":precheck["reason"], + "truncated":precheck["truncated"], + "observations":positions + }); + if !valid(&evidence, scope, source) { + return Err("syntax_evidence_identity_or_position_invalid"); + } + Ok(evidence) +} + +/// 核对当前源码、固定 grammar 身份和每个 Unicode 标量坐标。 +/// 参数为不可信报告投影、工作区相对路径及当前源码;返回能否作为未核实任务证据。 +pub(crate) fn valid(evidence: &Value, scope: &str, source: &[u8]) -> bool { + let keys = [ + "schema_version", + "backend", + "language", + "scope", + "source_sha256", + "grammar_sha256", + "grammar_qualified", + "precheck_reason", + "truncated", + "observations", + ]; + let Some(rows) = evidence["observations"] + .as_array() + .filter(|rows| rows.len() <= 128) + else { + return false; + }; + let Some(language) = evidence["language"].as_str() else { + return false; + }; + if !evidence.as_object().is_some_and(|object| { + object.len() == keys.len() && keys.iter().all(|key| object.contains_key(*key)) + }) || evidence["schema_version"] != "1.0.0" + || evidence["backend"] != "bundled_tree_sitter_wasm_candidate" + || evidence["scope"] != scope + || evidence["source_sha256"] != format!("{:x}", Sha256::digest(source)) + || evidence["grammar_qualified"] != false + || !matches_language(scope, language) + || !evidence["truncated"].is_boolean() + || (evidence["truncated"] == true + && evidence["precheck_reason"] != "recovery_budget_truncated") + || (evidence["truncated"] == false + && evidence["precheck_reason"] != "grammar_version_unqualified") + { + return false; + } + let Ok(manifest) = bundled_grammar_candidates() else { + return false; + }; + if !manifest.assets.iter().any(|asset| { + asset.language == language + && asset.sha256 == evidence["grammar_sha256"] + && asset.release_status == "candidate_unvalidated" + }) { + return false; + } + let Ok(text) = std::str::from_utf8(source) else { + return false; + }; + let lines: Vec<&str> = text.split('\n').collect(); + rows.iter().all(|row| valid_position(row, &lines)) +} + +fn matches_language(scope: &str, language: &str) -> bool { + match language { + "typescript" => [".ts", ".mts", ".cts"] + .iter() + .any(|suffix| scope.ends_with(suffix)), + "tsx" => scope.ends_with(".tsx"), + _ => false, + } +} + +fn valid_position(row: &Value, lines: &[&str]) -> bool { + let keys = [ + "classification", + "kind", + "start_line", + "start_column", + "end_line", + "end_column", + "group_id", + ]; + let (Some(start_line), Some(start_column), Some(end_line), Some(end_column), Some(group_id)) = ( + row["start_line"].as_u64(), + row["start_column"].as_u64(), + row["end_line"].as_u64(), + row["end_column"].as_u64(), + row["group_id"].as_u64(), + ) else { + return false; + }; + row.as_object().is_some_and(|object| { + object.len() == keys.len() && keys.iter().all(|key| object.contains_key(*key)) + }) && row["classification"] == "suspected" + && matches!(row["kind"].as_str(), Some("ERROR" | "MISSING")) + && group_id > 0 + && start_line > 0 + && end_line >= start_line + && end_line <= lines.len() as u64 + && start_column > 0 + && end_column > 0 + && start_column <= lines[start_line as usize - 1].chars().count() as u64 + 1 + && end_column <= lines[end_line as usize - 1].chars().count() as u64 + 1 + && (start_line, start_column) <= (end_line, end_column) +} diff --git a/crates/codeguard-cli/src/eslint_workbench.rs b/crates/codeguard-cli/src/eslint_workbench.rs index 32d72e9e..b43f588b 100644 --- a/crates/codeguard-cli/src/eslint_workbench.rs +++ b/crates/codeguard-cli/src/eslint_workbench.rs @@ -24,30 +24,41 @@ pub(crate) fn connect( } pub(crate) fn connect_preparation(root: &Path, args: &EslintLintArguments, reason: &str) -> Value { match crate::eslint_preparation::prepare(root, args, reason) { - Ok(report) => { - let id = report["blocker_id"].as_str().unwrap_or("").to_owned(); - let mut result = persist(root, report, "node.eslint.preparation"); - if matches!( - reason, - "eslint_syntax_confirmation_needed" | "eslint_syntax_precheck_unavailable" - ) && result["status"] == "synced_partial" - { - if let Ok(brief) = crate::next_command::read_task_brief(root, &id) { - result["task_id"] = json!(id); - result["next"] = json!({ - "schema_version":"0.1.0","report_type":"repair_brief_preview", - "operation":"next","command_status":"complete","exit_code":0, - "disposition":brief["disposition"],"reason":"current_source_confirmation_task", - "repair_brief":brief,"next_actions":[], - "authority":"local_unverified","delivery_decision":"not_evaluated" - }); - } - } - result - } + Ok(report) => persist_preparation(root, report, reason), + Err(reason) => json!({"status":reason}), + } +} +pub(crate) fn connect_syntax_preparation( + root: &Path, + args: &EslintLintArguments, + precheck: &Value, +) -> Value { + match crate::eslint_preparation::prepare_with_syntax(root, args, precheck) { + Ok(report) => persist_preparation(root, report, "eslint_syntax_confirmation_needed"), Err(reason) => json!({"status":reason}), } } +fn persist_preparation(root: &Path, report: Value, reason: &str) -> Value { + let id = report["blocker_id"].as_str().unwrap_or("").to_owned(); + let mut result = persist(root, report, "node.eslint.preparation"); + if matches!( + reason, + "eslint_syntax_confirmation_needed" | "eslint_syntax_precheck_unavailable" + ) && result["status"] == "synced_partial" + { + if let Ok(brief) = crate::next_command::read_task_brief(root, &id) { + result["task_id"] = json!(id); + result["next"] = json!({ + "schema_version":"0.1.0","report_type":"repair_brief_preview", + "operation":"next","command_status":"complete","exit_code":0, + "disposition":brief["disposition"],"reason":"current_source_confirmation_task", + "repair_brief":brief,"next_actions":[], + "authority":"local_unverified","delivery_decision":"not_evaluated" + }); + } + } + result +} fn persist(root: &Path, report: Value, checker: &str) -> Value { if let Err(reason) = save_local_report(root, &report) { return json!({"status":reason}); diff --git a/crates/codeguard-cli/src/lib.rs b/crates/codeguard-cli/src/lib.rs index 2cd146a7..8b216fdc 100644 --- a/crates/codeguard-cli/src/lib.rs +++ b/crates/codeguard-cli/src/lib.rs @@ -26,6 +26,7 @@ pub mod eslint_probe_request; pub mod eslint_probe_result; #[cfg(unix)] mod eslint_project_context; +mod eslint_syntax_evidence; mod eslint_task_recheck; #[cfg(unix)] mod eslint_workbench; diff --git a/crates/codeguard-cli/src/work_sync.rs b/crates/codeguard-cli/src/work_sync.rs index 31f0d960..54874ceb 100644 --- a/crates/codeguard-cli/src/work_sync.rs +++ b/crates/codeguard-cli/src/work_sync.rs @@ -452,6 +452,10 @@ fn import_one( codeguard_adapters::parse_unique_json(&bytes) .map_err(|_| "npm_report_duplicate_or_invalid_json")?; } + if value["report_type"] == "eslint_preparation_observation" { + codeguard_adapters::parse_unique_json(&bytes) + .map_err(|_| "eslint_preparation_duplicate_or_invalid_json")?; + } // 已消费的历史报告按原字节收据确认,不用当前源码重演历史输入。 if matches!( value["report_type"].as_str(), @@ -2030,6 +2034,14 @@ fn render_blocker_task(report: &ReportInput, blocker: &BlockerInput) -> String { blocker.id, blocker.reason, report.digest, report.run_id ); } + if blocker.checker_id == "node.eslint.preparation" + && blocker.diagnostic_reason.as_deref() == Some("eslint_syntax_confirmation_needed") + { + return format!( + "# {} TypeScript 语法原生确认任务\n\n- 问题证据:源码范围 `{}` 的候选 WASM 初检尚未验收;脱敏疑似位置、源码及 grammar 摘要在 `.codeguard/reports/{}.json` 的 `syntax_evidence`,报告 SHA-256 `{}`。这些不是已确认源码违规。\n- 规则依据:Tree-sitter 恢复节点只提示疑似语法位置;原生 TypeScript/ESLint 能力和项目原配置仍须确认。\n- 允许范围:核对本源码、对应构建根、原生工具及配置;不得仅凭 WASM 恢复节点修改源码、增加忽略或白名单。\n- 修复步骤:先查看同 run 的脱敏位置,再恢复适用的原生语法检查;若原生反证,保留证据并进入 grammar 误报调查。\n- 复检命令:codeguard lint typescript <原源码> --workspace <原工作区> --node-tool <核验绝对路径> --eslint-entry <核验绝对路径> --eslint-version <核验版本> --config <原配置绝对路径> --cwd <原工作目录绝对路径>。\n- 历史尝试:首次 run `{}`;后续观察、尝试和复检保留在同一任务,首张 Markdown 不是完整历史。\n- 关闭条件:同输入、范围、方言及语法能力的原生复检和正式策略/覆盖核验均满足;安装、WASM 零恢复或任务勾选不能关闭。\n", + blocker.id, blocker.scope, report.run_id, report.digest, report.run_id + ); + } if blocker.checker_id == "node.eslint.preparation" { return format!( "# {} ESLint 检查完整性任务\n\n- 问题证据:诊断 `{}`,报告摘要 `{}`。\n- 规则依据:原工具、配置和报告检查完整性;不是已确认源码违规。\n- 允许范围:核对Node、ESLint入口/版本、原配置、parser/插件及报告归属;不修改无关源码。\n- 修复步骤:恢复缺失上下文;若为解析或抑制诊断,先复现并定位根因,不默认添加忽略或白名单。\n- 复检:codeguard lint typescript <原源码> --workspace <原工作区> --node-tool <核验绝对路径> --eslint-entry <核验绝对路径> --eslint-version <核验版本> --config <原配置绝对路径> --cwd <原工作目录绝对路径>。\n- 历史尝试:首次run `{}`,后续诊断保留在同一任务观察中。\n- 关闭条件:本来受阻的原检查恢复并完成正式复检;任务勾选和局部成功不能关闭。\n", diff --git a/crates/codeguard-cli/src/work_sync/eslint_report.rs b/crates/codeguard-cli/src/work_sync/eslint_report.rs index a5550696..5de9d77f 100644 --- a/crates/codeguard-cli/src/work_sync/eslint_report.rs +++ b/crates/codeguard-cli/src/work_sync/eslint_report.rs @@ -197,13 +197,23 @@ pub(super) fn parse_preparation( .as_str() .ok_or("preparation_scope_invalid")?; let source = root.join(relative); - if source.canonicalize().ok().as_deref() != Some(source.as_path()) - || read_bounded_regular_file(&source, 16 * 1024 * 1024) - .ok() - .is_none_or(|bytes| report["source_sha256"] != format!("{:x}", Sha256::digest(bytes))) - { + if source.canonicalize().ok().as_deref() != Some(source.as_path()) { + return Err("eslint_preparation_source_changed"); + } + let source_bytes = read_bounded_regular_file(&source, 16 * 1024 * 1024) + .map_err(|_| "eslint_preparation_source_changed")?; + if report["source_sha256"] != format!("{:x}", Sha256::digest(&source_bytes)) { return Err("eslint_preparation_source_changed"); } + if report["schema_version"] == "0.2.0" + && !crate::eslint_syntax_evidence::valid( + &report["syntax_evidence"], + relative, + &source_bytes, + ) + { + return Err("eslint_syntax_evidence_invalid"); + } Ok(ReportInput { workspace_id: workspace.into(), run_id: run.into(), diff --git a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs index 08fb3b2d..2fb782e5 100644 --- a/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs +++ b/crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs @@ -159,6 +159,72 @@ fn initialized_wasm_precheck_keeps_one_native_confirmation_task() { assert_eq!(fact["kind"], "blocker"); assert_eq!(fact["checker_id"], "node.eslint.preparation"); assert_eq!(fact["state"], "open"); + let stored: Value = fs::read_dir(root.join(".codeguard/reports")) + .unwrap() + .map(|entry| { + serde_json::from_slice::(&fs::read(entry.unwrap().path()).unwrap()).unwrap() + }) + .find(|report| report["diagnostic_reason"] == "eslint_syntax_confirmation_needed") + .unwrap(); + assert_eq!(stored["schema_version"], "0.2.0"); + let stored_schema: Value = serde_json::from_str(include_str!( + "../../../schemas/eslint-preparation-observation-v0.2.schema.json" + )) + .unwrap(); + assert_eq!(stored_schema["additionalProperties"], false); + assert_eq!( + stored + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect::>(), + stored_schema["required"] + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap()) + .collect() + ); + assert!(!stored.to_string().contains("const x")); + let task_text = + fs::read_to_string(root.join(".codeguard/tasks").join(format!("{task_id}.md"))).unwrap(); + assert!(task_text.contains("syntax_evidence")); + assert!(task_text.contains(stored["run_id"].as_str().unwrap())); + assert_eq!( + stored["syntax_evidence"]["source_sha256"], + stored["source_sha256"] + ); + assert_eq!( + stored["syntax_evidence"]["grammar_sha256"], + first["syntax_precheck"]["grammar_sha256"] + ); + assert_eq!( + stored["syntax_evidence"]["observations"] + .as_array() + .unwrap() + .len(), + first["syntax_precheck"]["observations"] + .as_array() + .unwrap() + .len() + ); + assert_eq!( + first["workbench"]["next"]["repair_brief"]["preparation_guidance"]["suspected_positions"] + [0]["start_line"], + first["syntax_precheck"]["observations"][0]["start_line"] + ); + let next = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args(["next", root.to_str().unwrap(), "--format", "json"]) + .output() + .unwrap(); + assert_eq!(next.status.code(), Some(0)); + let next: Value = serde_json::from_slice(&next.stdout).unwrap(); + assert_eq!(next["repair_brief"]["task_id"], task_id); + assert_eq!( + next["repair_brief"]["preparation_guidance"]["suspected_positions"][0]["start_line"], + first["syntax_precheck"]["observations"][0]["start_line"] + ); let second = lint(); assert_eq!(second["setup"]["task_id"], task_id); assert_eq!(second["workbench"]["new_blockers"], 0); @@ -239,6 +305,89 @@ fn initialized_workspace_keeps_precheck_failure_as_native_setup_task() { ); } +#[test] +fn forged_syntax_task_evidence_cannot_be_imported_as_a_source_finding() { + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + let source = root.join("bad.ts"); + fs::write(&source, "const x: number = ;\n").unwrap(); + let init = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "init", + root.to_str().unwrap(), + "--apply", + "--format", + "json", + ]) + .output() + .unwrap(); + assert_eq!(init.status.code(), Some(3)); + let first = 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!(first.status.code(), Some(3)); + let first: Value = serde_json::from_slice(&first.stdout).unwrap(); + let task_id = first["setup"]["task_id"].as_str().unwrap(); + let reports = root.join(".codeguard/reports"); + let original: Value = serde_json::from_slice( + &fs::read( + fs::read_dir(&reports) + .unwrap() + .next() + .unwrap() + .unwrap() + .path(), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(original["schema_version"], "0.2.0"); + + let mut wrong_grammar = original.clone(); + wrong_grammar["run_id"] = serde_json::json!("eslint-preparation-999-99999999999999999991"); + wrong_grammar["syntax_evidence"]["grammar_sha256"] = serde_json::json!("0".repeat(64)); + fs::write( + reports.join("eslint-preparation-999-99999999999999999991.json"), + serde_json::to_vec_pretty(&wrong_grammar).unwrap(), + ) + .unwrap(); + let mut wrong_position = original; + wrong_position["run_id"] = serde_json::json!("eslint-preparation-999-99999999999999999992"); + wrong_position["syntax_evidence"]["observations"][0]["start_line"] = serde_json::json!(999999); + fs::write( + reports.join("eslint-preparation-999-99999999999999999992.json"), + serde_json::to_vec_pretty(&wrong_position).unwrap(), + ) + .unwrap(); + let sync = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args(["work", "sync", root.to_str().unwrap(), "--format", "json"]) + .output() + .unwrap(); + let summary: Value = serde_json::from_slice(&sync.stdout).unwrap(); + assert_eq!(summary["failed_reports"], 2); + assert_eq!(summary["new_findings"], 0); + assert_eq!(summary["new_blockers"], 0); + let fact: Value = serde_json::from_slice( + &fs::read( + root.join(".codeguard/findings") + .join(task_id) + .join("finding.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(fact["state"], "open"); +} + #[test] fn no_recovery_remains_incomplete_and_javascript_does_not_use_typescript_grammar() { let project = Project::new(); diff --git a/docs/Codeguard-Command-Reference.md b/docs/Codeguard-Command-Reference.md index 752a220a..82fc1a05 100644 --- a/docs/Codeguard-Command-Reference.md +++ b/docs/Codeguard-Command-Reference.md @@ -12,7 +12,7 @@ Current slices include static discovery/init, selected native checks, partial ag 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). -With `--workspace` pointing to an initialized, canonical workspace, the TypeScript/TSX fallback emits [feedback 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json). It saves one source-scoped ESLint native-confirmation blocker and returns its real `setup.task_id` only after successful sync. Repeated scans and a later WASM result with no recovery nodes keep that task open; the individual suspected positions still need durable linkage and capability-matched native verification. See the [workbench acceptance record](../tests/acceptance/typescript-syntax-confirmation-task.md). +With `--workspace` pointing to an initialized, canonical workspace, the TypeScript/TSX fallback emits [feedback 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json). It saves one source-scoped ESLint native-confirmation blocker and returns its real `setup.task_id` only after successful sync. Repeated scans and a later WASM result with no recovery nodes keep that task open; a versioned local report now links bounded suspected positions to that task through its report digest, while capability-matched native verification remains unfinished. See the [workbench acceptance record](../tests/acceptance/typescript-syntax-confirmation-task.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 d1846444..22ab0234 100644 --- a/docs/Codeguard-Command-Reference.zh_CN.md +++ b/docs/Codeguard-Command-Reference.zh_CN.md @@ -22,7 +22,7 @@ [已验收实现切片](../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与闭环承诺均为目标契约。 -对已初始化且使用规范绝对路径的 `--workspace`,TypeScript/TSX 候选回退改用[反馈 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json):按源码范围保存同一张 ESLint 原生确认阻塞任务,仅同步成功时返回真实 `setup.task_id`。重复扫描和后续 WASM 无恢复节点不关闭任务。逐个疑似位置的持久关联及能力匹配原生复检仍未接通,见[局部验收](../tests/acceptance/typescript-syntax-confirmation-task.md)。 +对已初始化且使用规范绝对路径的 `--workspace`,TypeScript/TSX 候选回退改用[反馈 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json):按源码范围保存同一张 ESLint 原生确认阻塞任务,仅同步成功时返回真实 `setup.task_id`。重复扫描和后续 WASM 无恢复节点不关闭任务。版本化本地报告已通过报告摘要关联有界疑似位置;能力匹配原生复检仍未接通,见[局部验收](../tests/acceptance/typescript-syntax-confirmation-task.md)。 ## 1. 命令体系与协作路线 diff --git a/docs/Codeguard-Technical-Design.md b/docs/Codeguard-Technical-Design.md index 835c0e24..73f0eba2 100644 --- a/docs/Codeguard-Technical-Design.md +++ b/docs/Codeguard-Technical-Design.md @@ -183,7 +183,7 @@ Core now has a local pure setup-action selector taking an upstream-verified nati When some files have suspected issues and others are unresolved, use overall `incomplete` while retaining all usable suspected observations. `clean` requires nonempty, completely checked selected scope. -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. With an initialized workspace, TypeScript/TSX now uses [feedback 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json) and attaches one stable native-confirmation task to the source scope; individual recovery positions are not yet persisted into that task, and host rendering remains 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. +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. With an initialized workspace, TypeScript/TSX now uses [feedback 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json) and attaches one stable native-confirmation task to the source scope; the [0.2.0 preparation report](../schemas/eslint-preparation-observation-v0.2.schema.json) stores bounded suspected positions with source and grammar identities and links them by digest to the task. Host rendering and capability-matched native closure 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. 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). diff --git a/docs/Codeguard-Technical-Design.zh_CN.md b/docs/Codeguard-Technical-Design.zh_CN.md index fc456289..47d9345f 100644 --- a/docs/Codeguard-Technical-Design.zh_CN.md +++ b/docs/Codeguard-Technical-Design.zh_CN.md @@ -183,7 +183,7 @@ flowchart LR 部分文件有疑似异常、另有文件未解析时,总体为 `incomplete`,同时保留所有可用疑似观察。`clean` 要求选定范围非空且全部完成检查。 -这些是目标语义字段,尚非已发行的项目级 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) 单文件生产端已提供恢复位置和准备指引。TypeScript/TSX 在已初始化工作区改用[反馈 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json),按源码范围关联一张稳定的原生确认任务;逐个恢复位置尚未持久关联到任务,宿主渲染也未接通。内置 grammar 仍是未验收候选,因此不能返回 `clean`。保持既有退出语义:必需原生执行缺失仍为未完成(`3`);解析器疑似问题本身不是已确认违规(`1`)。若未来增加独立语法操作,其成功必须限定为语法初检;本文不宣称已有 `syntax` 命令。 +这些是目标语义字段,尚非已发行的项目级 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) 单文件生产端已提供恢复位置和准备指引。TypeScript/TSX 在已初始化工作区改用[反馈 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json),按源码范围关联一张稳定的原生确认任务;[0.2.0 准备报告](../schemas/eslint-preparation-observation-v0.2.schema.json)以源码和 grammar 身份保存有界疑似位置,并通过报告摘要关联到任务;宿主渲染和能力匹配原生关闭仍未接通。内置 grammar 仍是未验收候选,因此不能返回 `clean`。保持既有退出语义:必需原生执行缺失仍为未完成(`3`);解析器疑似问题本身不是已确认违规(`1`)。若未来增加独立语法操作,其成功必须限定为语法初检;本文不宣称已有 `syntax` 命令。 可选特性构建的 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)。 diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index ca72cd77..8ad815cf 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/tasks.md +++ b/openspec/changes/introduce-rust-codeguard-cli/tasks.md @@ -1034,10 +1034,10 @@ TDD缺自动同步先RED。相关普通:build9、work sync15、next6,共30 - [ ] 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);单文件 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.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) 验收。TypeScript/TSX 疑似位置现可进入已初始化工作台的版本化任务报告;正式全范围/嵌入区域/排除原因、Java 与多模块任务引用、所有消费者未知 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。未初始化时任务引用为空;TypeScript/TSX 在已初始化工作区同步成功后用 0.5.0 报告返回真实稳定任务 ID,见[工作台局部验收](../../../tests/acceptance/typescript-syntax-confirmation-task.md)。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)。配置歧义、链接入口和损坏包清单连续出现时亦更新同一 ESLint 环境任务、变更具体诊断而不生成源码 finding;见同一[局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。TypeScript/TSX 候选 WASM 初检现在将已初始化工作区的源码范围接到一张稳定的 ESLint 原生确认阻塞任务;重复扫描和后续零恢复节点不关闭,见[工作台局部验收](../../../tests/acceptance/typescript-syntax-confirmation-task.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)。配置歧义、链接入口和损坏包清单连续出现时亦更新同一 ESLint 环境任务、变更具体诊断而不生成源码 finding;见同一[局部验收](../../../tests/acceptance/native-first-eslint-candidate.md)。TypeScript/TSX 候选 WASM 初检现在将已初始化工作区的源码范围接到一张稳定的 ESLint 原生确认阻塞任务;重复扫描和后续零恢复节点不关闭,见[工作台局部验收](../../../tests/acceptance/typescript-syntax-confirmation-task.md)。TypeScript/TSX 有界疑似位置已用源码/grammar 身份保存到 0.2.0 准备报告并由任务报告摘要引用,导入拒绝伪造 grammar 和越界坐标;失败尝试/无进展预算、推荐项非阻断投影、Java 与多模块任务尚缺,不勾选。 - [ ] 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/schemas/eslint-preparation-observation-v0.2.schema.json b/schemas/eslint-preparation-observation-v0.2.schema.json new file mode 100644 index 00000000..72ec670a --- /dev/null +++ b/schemas/eslint-preparation-observation-v0.2.schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:codeguard:eslint-preparation-observation:0.2.0", + "title": "Local unverified ESLint syntax confirmation evidence", + "description": "Structural schema only. The Rust importer also binds source bytes, workspace, grammar identity and positions.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "report_type", "workspace_binding", "workspace_id", "run_id", + "checker_id", "authority", "coverage_proven", "delivery_decision", "execution", + "blocker_id", "fingerprint", "reason_code", "diagnostic_reason", "build_root", + "scope", "affected_paths", "source_sha256", "syntax_evidence" + ], + "properties": { + "schema_version": {"const": "0.2.0"}, + "report_type": {"const": "eslint_preparation_observation"}, + "workspace_binding": {"const": "bound"}, + "workspace_id": {"type": "string", "minLength": 1}, + "run_id": {"type": "string", "pattern": "^eslint-preparation-[1-9][0-9]*-[1-9][0-9]*$"}, + "checker_id": {"const": "node.eslint.preparation"}, + "authority": {"const": "local_unverified"}, + "coverage_proven": {"const": false}, + "delivery_decision": {"const": "not_evaluated"}, + "execution": {"const": "incomplete"}, + "blocker_id": {"type": "string", "pattern": "^CG-B-[0-9a-f]{32}$"}, + "fingerprint": {"$ref": "#/$defs/digest"}, + "reason_code": {"const": "eslint_prerequisites_require_review"}, + "diagnostic_reason": {"const": "eslint_syntax_confirmation_needed"}, + "build_root": {"const": "."}, + "scope": {"type": "string", "minLength": 1}, + "affected_paths": {"type": "array", "minItems": 1, "maxItems": 1, "items": {"type": "string", "minLength": 1}}, + "source_sha256": {"$ref": "#/$defs/digest"}, + "syntax_evidence": {"$ref": "#/$defs/syntax_evidence"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "syntax_evidence": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "backend", "language", "scope", "source_sha256", + "grammar_sha256", "grammar_qualified", "precheck_reason", "truncated", "observations" + ], + "properties": { + "schema_version": {"const": "1.0.0"}, + "backend": {"const": "bundled_tree_sitter_wasm_candidate"}, + "language": {"enum": ["typescript", "tsx"]}, + "scope": {"type": "string", "minLength": 1}, + "source_sha256": {"$ref": "#/$defs/digest"}, + "grammar_sha256": {"$ref": "#/$defs/digest"}, + "grammar_qualified": {"const": false}, + "precheck_reason": {"enum": ["grammar_version_unqualified", "recovery_budget_truncated"]}, + "truncated": {"type": "boolean"}, + "observations": {"type": "array", "maxItems": 128, "items": {"$ref": "#/$defs/observation"}} + } + }, + "observation": { + "type": "object", + "additionalProperties": false, + "required": ["classification", "kind", "start_line", "start_column", "end_line", "end_column", "group_id"], + "properties": { + "classification": {"const": "suspected"}, + "kind": {"enum": ["ERROR", "MISSING"]}, + "start_line": {"type": "integer", "minimum": 1}, + "start_column": {"type": "integer", "minimum": 1}, + "end_line": {"type": "integer", "minimum": 1}, + "end_column": {"type": "integer", "minimum": 1}, + "group_id": {"type": "integer", "minimum": 1} + } + } + } +} diff --git a/tests/acceptance/typescript-syntax-confirmation-task.md b/tests/acceptance/typescript-syntax-confirmation-task.md index e24802be..4b9123ec 100644 --- a/tests/acceptance/typescript-syntax-confirmation-task.md +++ b/tests/acceptance/typescript-syntax-confirmation-task.md @@ -4,6 +4,8 @@ 先写端到端反例:已初始化工作区的语法初检返回 `workbench=not_connected`,断言失败。接线后,连续两次对含语法恢复节点的源码初检,只产生一个 `node.eslint.preparation`、`kind=blocker` 的任务,第二次新增阻塞数为零;把源码改成无恢复节点后仍复用同一开放任务,`delivery_decision=not_evaluated`。超过 1 MiB 的源码使初检未运行时,也保留单独的未完成诊断和真实环境任务,不产生源码 finding。`next_action` 明确要求同一输入的适用原生语法复检,不能把 WASM 零恢复、安装成功或任务勾选当作关闭证据。工作台不可用时不填虚构任务 ID。 -当前持久报告只保存源码范围、源码 SHA-256、前置诊断和报告摘要;CLI 当轮反馈显示的**逐个疑似位置尚未持久关联**到该任务。任务也没有能力匹配的原生关闭收据,Java 初检和多模块调度未接入这条工作台路径。因此 14.9、14.10、14.11 均不勾选,不能据此宣称完整修复闭环或语法能力已发布。 +持久准备报告现升级为[0.2.0](../../schemas/eslint-preparation-observation-v0.2.schema.json):仅在 worker 完成候选解析时记录工作区相对范围、源码 SHA-256、固定 grammar SHA-256、ERROR/MISSING 类别和最多 128 个 Unicode 标量坐标,不保存源码片段或任意语法节点文本。任务 Markdown 引用本轮报告路径与摘要,`next` 的 `preparation_guidance.suspected_positions` 返回同一份经当前源码核对的疑似位置。重复扫描仍更新同一任务;源码变化后旧报告只能留作历史,不能给新源码提供当前确认依据。手工伪造的 grammar 摘要或越界位置在 `work sync` 被拒绝,不生成源码 finding 或关闭任务。 -定向验证:`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`,共 21 项通过、4 项要求真实 ESLint 10 的用例忽略;新增端到端反例覆盖 TypeScript 重扫、源码改动后不自动关闭、TSX 独立任务、初检未运行与 `next` 和 `setup.task_id` 一致。`cargo clippy --locked -p codeguard-cli --features wasm-precheck --all-targets -- -D warnings`、`cargo fmt --all -- --check`、`python3 scripts/check_layering.py`、`openspec validate introduce-rust-codeguard-cli --strict` 与 `git diff --check` 均通过。真实 ESLint 10 运行、完整工作区 CI 及宿主对话验收不由这些局部测试证明。 +这些位置仍为本地未核实的候选证据,不能证明 grammar 在该语言版本上正确,也不能证明原生检查结果。任务尚无能力匹配的原生关闭收据,Java 初检、多模块调度和宿主对话未接入这条路径。因此 14.9、14.10、14.11 均不勾选,不能据此宣称完整修复闭环或语法能力已发布。 + +定向验证:`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`,共 22 项通过、4 项要求真实 ESLint 10 的用例忽略;新增端到端反例覆盖 TypeScript 重扫、源码改动后不自动关闭、TSX 独立任务、初检未运行、版本化脱敏位置关联、错 grammar/越界坐标拒绝与 `next` 和 `setup.task_id` 一致。`cargo clippy --locked -p codeguard-cli --features wasm-precheck --all-targets -- -D warnings`、`cargo fmt --all -- --check`、`python3 scripts/check_layering.py`、`openspec validate introduce-rust-codeguard-cli --strict` 与 `git diff --check` 均通过。真实 ESLint 10 运行、完整工作区 CI 及宿主对话验收不由这些局部测试证明。