From 2fc1bfb9400bb336e01fed0889da1a920eb5526f Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:32:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=20Python=20WASM=20=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E7=A1=AE=E8=AE=A4=E7=A8=B3=E5=AE=9A=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E4=B8=8E=E5=A4=8D=E6=89=AB=E8=AF=81=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README.zh-CN.md | 2 +- crates/codeguard-cli/src/lib.rs | 2 + crates/codeguard-cli/src/next_command.rs | 10 +- .../codeguard-cli/src/python_lint_command.rs | 20 + .../src/python_syntax_confirmation.rs | 264 +++++ crates/codeguard-cli/src/work_sync.rs | 16 + .../python_syntax_confirmation_report.rs | 46 + crates/codeguard-cli/tests/lint_python_cli.rs | 2 +- .../tests/python_syntax_fallback_candidate.rs | 199 ++++ docs/Codeguard-Architecture.md | 2 +- docs/Codeguard-Architecture.zh_CN.md | 2 +- docs/Codeguard-Command-Reference.md | 2 +- docs/Codeguard-Command-Reference.zh_CN.md | 2 +- docs/Codeguard-Technical-Design.md | 2 +- docs/Codeguard-Technical-Design.zh_CN.md | 2 +- .../introduce-rust-codeguard-cli/tasks.md | 6 +- .../verification.md | 6 + .../python-lint-feedback-v0.15.schema.json | 949 ++++++++++++++++++ ...-confirmation-observation-v0.1.schema.json | 164 +++ .../python-syntax-fallback-candidate.md | 10 +- 21 files changed, 1693 insertions(+), 17 deletions(-) create mode 100644 crates/codeguard-cli/src/python_syntax_confirmation.rs create mode 100644 crates/codeguard-cli/src/work_sync/python_syntax_confirmation_report.rs create mode 100644 schemas/python-lint-feedback-v0.15.schema.json create mode 100644 schemas/python-syntax-confirmation-observation-v0.1.schema.json diff --git a/README.md b/README.md index 5de1c7a1..58fa8d50 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` 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. Python `lint` first preserves Ruff results, then can attach suspected positions for missing Ruff or undeclared project Ruff configuration; a broken Ruff configuration stays a setup blocker. 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). +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. Python `lint` first preserves Ruff results, then can attach suspected positions for missing Ruff or undeclared project Ruff configuration; a broken Ruff configuration stays a setup blocker. For one checked file in an initialized workspace, the source-only path syncs one stable native-confirmation task; repeat candidate scans do not close it. 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. 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). diff --git a/README.zh-CN.md b/README.zh-CN.md index feffcdd2..d7a59ae6 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 兜底或自动对话交付。** 保留已有命令作为统一入口;当前仍须满足各原生适配器的参数和前置条件: -可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口;仅本地 ESLint 包未观察到时才提供 TypeScript/TSX 疑似语法初检。入口、包身份或配置不可信时保留具体环境阻塞。Java 候选仍须原生确认。Python `lint` 先保留 Ruff 结果,在 Ruff 缺失或项目未声明配置时补充疑似位置;损坏的 Ruff 配置仍是环境阻塞。所有内置 grammar 的版本/方言尚未验收,原生局部观察和初检都不能批准交付。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md)、[TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)与[原生优先局部验收](tests/acceptance/native-first-eslint-candidate.md)。 +可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口;仅本地 ESLint 包未观察到时才提供 TypeScript/TSX 疑似语法初检。入口、包身份或配置不可信时保留具体环境阻塞。Java 候选仍须原生确认。Python `lint` 先保留 Ruff 结果,在 Ruff 缺失或项目未声明配置时补充疑似位置;损坏的 Ruff 配置仍是环境阻塞。已初始化工作区的单文件初检可同步一张稳定的原生确认任务;重复候选扫描不会关闭它。所有内置 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 扫描不会自行关闭任务。版本化本地报告已按源码与 grammar 身份保存有界疑似位置;能力匹配的原生关闭仍待实现,见[局部验收](tests/acceptance/typescript-syntax-confirmation-task.md)。 diff --git a/crates/codeguard-cli/src/lib.rs b/crates/codeguard-cli/src/lib.rs index d1356042..3f701f1e 100644 --- a/crates/codeguard-cli/src/lib.rs +++ b/crates/codeguard-cli/src/lib.rs @@ -148,6 +148,8 @@ pub(crate) mod python_cve_task_recheck; pub mod python_lint_command; #[cfg(unix)] pub mod python_lint_scan; +#[cfg(unix)] +mod python_syntax_confirmation; #[cfg(all(unix, feature = "wasm-precheck"))] mod python_syntax_precheck; pub mod quality_policy_candidate; diff --git a/crates/codeguard-cli/src/next_command.rs b/crates/codeguard-cli/src/next_command.rs index 7bc198b7..ef8320c2 100644 --- a/crates/codeguard-cli/src/next_command.rs +++ b/crates/codeguard-cli/src/next_command.rs @@ -530,7 +530,15 @@ fn candidate(root: &Path, id: &str, fact: &Value) -> Result ExitCode { "候选 Python grammar 版本尚未验收;确认项目要求的原生检查器及配置后复检,不把零恢复节点当作通过" }.into(), ); + if feedback["workspace_binding"] == "bound" { + let task = crate::python_syntax_confirmation::persist(&root, &precheck, deadline); + feedback["schema_version"] = Value::String("0.15.0".into()); + match task { + Ok(id) => { + feedback["setup"]["task_id"] = Value::String(id); + feedback["task_persistence"] = + serde_json::json!({"status":"synced_partial","reason":null}); + } + Err(reason) => { + feedback["task_persistence"] = + serde_json::json!({"status":"unavailable","reason":reason}); + } + } + } feedback["syntax_precheck"] = precheck; } } @@ -806,6 +821,11 @@ fn print_human(feedback: &Value) { .as_str() .unwrap_or("复查原生检查条件") ); + if let Some(id) = feedback["setup"]["task_id"].as_str() { + println!("原生确认任务:{id}"); + } else if let Some(reason) = feedback["task_persistence"]["reason"].as_str() { + println!("确认任务未保存:{reason}"); + } } println!("本命令尚无批准策略与完整义务,交付判定:not_evaluated"); } diff --git a/crates/codeguard-cli/src/python_syntax_confirmation.rs b/crates/codeguard-cli/src/python_syntax_confirmation.rs new file mode 100644 index 00000000..dc66a54c --- /dev/null +++ b/crates/codeguard-cli/src/python_syntax_confirmation.rs @@ -0,0 +1,264 @@ +//! Python 候选语法观察到既有工作台的稳定原生确认任务。 + +#[cfg(feature = "wasm-precheck")] +use crate::work_sync::{save_local_report, sync_local_workspace}; +#[cfg(feature = "wasm-precheck")] +use crate::workspace_refresh::read_workspace_baseline; +use codeguard_adapters::bundled_grammar_candidates; +use codeguard_runtime::read_bounded_regular_file; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::path::{Component, Path}; +#[cfg(feature = "wasm-precheck")] +use std::time::{Instant, SystemTime, UNIX_EPOCH}; + +const MAX_SOURCE_BYTES: u64 = 1024 * 1024; +const MAX_OBSERVATIONS: usize = 128; + +/// 将一个已检查的 Python 文件关联到稳定的原生语法确认任务。 +/// 参数为规范化工作区和本轮候选观察;返回真实落盘任务 ID,写入或同步失败则保留具体原因。 +#[cfg(feature = "wasm-precheck")] +pub(crate) fn persist( + root: &Path, + precheck: &Value, + deadline: Instant, +) -> Result { + if Instant::now() >= deadline || codeguard_runtime::sigint_cancellation_requested() { + return Err("request_deadline_or_cancelled"); + } + let baseline = read_workspace_baseline(root).map_err(|_| "workspace_invalid")?; + let workspace = baseline + .as_ref() + .and_then(|value| value.workspace_id()) + .ok_or("workspace_not_initialized")?; + if precheck["selected_files"] != 1 || precheck["checked_files"] != 1 { + return Err("confirmation_scope_not_single_file"); + } + let checked = precheck["checked"] + .as_array() + .filter(|items| items.len() == 1) + .and_then(|items| items.first()) + .ok_or("checked_source_missing")?; + let scope = checked["path"] + .as_str() + .filter(|value| safe_python_path(value)) + .ok_or("checked_source_invalid")?; + let source = root.join(scope); + if source.canonicalize().ok().as_deref() != Some(source.as_path()) { + return Err("checked_source_changed"); + } + let bytes = read_bounded_regular_file(&source, MAX_SOURCE_BYTES) + .map_err(|_| "checked_source_unavailable")?; + let source_sha = format!("{:x}", Sha256::digest(&bytes)); + if checked["source_sha256"] != source_sha { + return Err("checked_source_changed"); + } + let grammar_sha = bundled_grammar_candidates() + .map_err(|_| "grammar_manifest_unavailable")? + .assets + .into_iter() + .find(|asset| asset.language == "python") + .map(|asset| asset.sha256) + .ok_or("python_grammar_unavailable")?; + if checked["grammar_sha256"] != grammar_sha || precheck["grammar_sha256"] != grammar_sha { + return Err("grammar_identity_changed"); + } + let observations = precheck["observations"] + .as_array() + .filter(|items| items.len() <= MAX_OBSERVATIONS) + .ok_or("syntax_observations_invalid")?; + if !observations + .iter() + .all(|row| row["path"] == scope && row["source_sha256"] == source_sha) + { + return Err("syntax_observations_invalid"); + } + let fingerprint = fingerprint(workspace, scope); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| "clock_unavailable")? + .as_nanos(); + let report = json!({ + "schema_version":"0.1.0", + "report_type":"python_syntax_confirmation_observation", + "workspace_binding":"bound", + "workspace_id":workspace, + "run_id":format!("python-syntax-{}-{nanos}",std::process::id()), + "checker_id":"python.ruff", + "authority":"local_unverified", + "coverage_proven":false, + "delivery_decision":"not_evaluated", + "execution":"incomplete", + "reason_code":"python_syntax_confirmation_needed", + "blocker_id":format!("CG-B-{}",&fingerprint[..32]), + "fingerprint":fingerprint, + "build_root":".", + "scope":scope, + "affected_paths":[scope], + "source_sha256":source_sha, + "grammar_sha256":grammar_sha, + "observations":observations, + }); + if !valid_report(root, workspace, &report) { + return Err("syntax_confirmation_report_invalid"); + } + if Instant::now() >= deadline || codeguard_runtime::sigint_cancellation_requested() { + return Err("request_deadline_or_cancelled"); + } + save_local_report(root, &report)?; + let summary = sync_local_workspace(root)?; + if summary.failed_reports != 0 { + return Err("syntax_confirmation_sync_incomplete"); + } + let id = report["blocker_id"].as_str().ok_or("blocker_id_invalid")?; + if !root + .join(".codeguard/tasks") + .join(format!("{id}.md")) + .is_file() + { + return Err("syntax_confirmation_task_missing"); + } + Ok(id.into()) +} + +/// 校验本地候选报告身份、源码快照和有界位置;仅允许生成待确认任务。 +pub(crate) fn valid_report(root: &Path, workspace: &str, report: &Value) -> bool { + let keys = [ + "schema_version", + "report_type", + "workspace_binding", + "workspace_id", + "run_id", + "checker_id", + "authority", + "coverage_proven", + "delivery_decision", + "execution", + "reason_code", + "blocker_id", + "fingerprint", + "build_root", + "scope", + "affected_paths", + "source_sha256", + "grammar_sha256", + "observations", + ]; + let Some(scope) = report["scope"] + .as_str() + .filter(|scope| safe_python_path(scope)) + else { + return false; + }; + let fingerprint = fingerprint(workspace, scope); + let grammar = bundled_grammar_candidates().ok().and_then(|manifest| { + manifest + .assets + .into_iter() + .find(|asset| asset.language == "python") + }); + let source = root.join(scope); + let bytes = if source.canonicalize().ok().as_deref() == Some(source.as_path()) { + read_bounded_regular_file(&source, MAX_SOURCE_BYTES).ok() + } else { + None + }; + let source_sha = bytes + .as_ref() + .map(|bytes| format!("{:x}", Sha256::digest(bytes))); + let observations = report["observations"].as_array(); + report + .as_object() + .is_some_and(|map| map.len() == keys.len() && keys.iter().all(|key| map.contains_key(*key))) + && report["schema_version"] == "0.1.0" + && report["report_type"] == "python_syntax_confirmation_observation" + && report["workspace_binding"] == "bound" + && report["workspace_id"] == workspace + && report["run_id"].as_str().is_some_and(|run| { + run.strip_prefix("python-syntax-") + .and_then(|suffix| suffix.split_once('-')) + .is_some_and(|(pid, nanos)| { + pid.parse::().is_ok_and(|value| value > 0) + && nanos.parse::().is_ok_and(|value| value > 0) + }) + }) + && report["checker_id"] == "python.ruff" + && report["authority"] == "local_unverified" + && report["coverage_proven"] == false + && report["delivery_decision"] == "not_evaluated" + && report["execution"] == "incomplete" + && report["reason_code"] == "python_syntax_confirmation_needed" + && report["blocker_id"] == format!("CG-B-{}", &fingerprint[..32]) + && report["fingerprint"] == fingerprint + && report["build_root"] == "." + && report["affected_paths"] == json!([scope]) + && source_sha + .as_deref() + .is_some_and(|sha| report["source_sha256"] == sha) + && grammar + .as_ref() + .is_some_and(|asset| report["grammar_sha256"] == asset.sha256) + && observations.is_some_and(|rows| { + rows.len() <= MAX_OBSERVATIONS + && rows + .iter() + .all(|row| valid_observation(row, scope, bytes.as_deref().unwrap_or_default())) + }) +} + +fn fingerprint(workspace: &str, scope: &str) -> String { + let mut hasher = Sha256::new(); + for part in [ + "codeguard-python-syntax-confirmation-v1", + workspace, + "python.ruff", + scope, + ] { + hasher.update((part.len() as u64).to_be_bytes()); + hasher.update(part.as_bytes()); + } + format!("{:x}", hasher.finalize()) +} + +fn safe_python_path(value: &str) -> bool { + let path = Path::new(value); + !value.is_empty() + && value.len() <= 512 + && !value.contains('\\') + && !value.chars().any(char::is_control) + && path + .components() + .all(|part| matches!(part, Component::Normal(_))) + && path + .extension() + .is_some_and(|ext| matches!(ext.to_str(), Some("py" | "pyw"))) +} + +fn valid_observation(row: &Value, scope: &str, source: &[u8]) -> bool { + let Some(text) = std::str::from_utf8(source).ok() else { + return false; + }; + let lines: Vec<&str> = text.split('\n').collect(); + let position = |line_key: &str, column_key: &str| -> Option<(usize, usize)> { + let line = usize::try_from(row[line_key].as_u64()?).ok()?; + let column = usize::try_from(row[column_key].as_u64()?).ok()?; + let content = lines.get(line.checked_sub(1)?)?.trim_end_matches('\r'); + (column >= 1 && column <= content.chars().count() + 1).then_some((line, column)) + }; + let (Some(start), Some(end)) = ( + position("start_line", "start_column"), + position("end_line", "end_column"), + ) else { + return false; + }; + row.as_object().is_some_and(|map| map.len() == 10) + && row["path"] == scope + && row["source_sha256"] == format!("{:x}", Sha256::digest(source)) + && row["classification"] == "suspected" + && matches!(row["kind"].as_str(), Some("ERROR" | "MISSING")) + && row["syntax_kind"].as_str().is_some_and(|kind| { + !kind.is_empty() && kind.len() <= 120 && !kind.chars().any(char::is_control) + }) + && row["group_id"].as_u64().is_some() + && start <= end +} diff --git a/crates/codeguard-cli/src/work_sync.rs b/crates/codeguard-cli/src/work_sync.rs index 54874ceb..86be60c8 100644 --- a/crates/codeguard-cli/src/work_sync.rs +++ b/crates/codeguard-cli/src/work_sync.rs @@ -25,6 +25,7 @@ mod eslint_report; mod npm_preparation_report; mod npm_report; mod python_cve_report; +mod python_syntax_confirmation_report; mod rust_build_report; mod rust_cve_report; mod rustdoc_report; @@ -456,6 +457,10 @@ fn import_one( codeguard_adapters::parse_unique_json(&bytes) .map_err(|_| "eslint_preparation_duplicate_or_invalid_json")?; } + if value["report_type"] == "python_syntax_confirmation_observation" { + codeguard_adapters::parse_unique_json(&bytes) + .map_err(|_| "python_syntax_confirmation_duplicate_or_invalid_json")?; + } // 已消费的历史报告按原字节收据确认,不用当前源码重演历史输入。 if matches!( value["report_type"].as_str(), @@ -470,6 +475,7 @@ fn import_one( | "checkstyle_preparation_recheck" | "rust_cve_workbench_observation" | "python_cve_workbench_observation" + | "python_syntax_confirmation_observation" ) ) { let run = value["run_id"] @@ -552,6 +558,9 @@ fn parse_report( report: &Value, digest: String, ) -> Result { + if report["report_type"] == "python_syntax_confirmation_observation" { + return python_syntax_confirmation_report::parse(root, workspace_id, path, report, digest); + } if report["report_type"] == "python_cve_workbench_observation" { return python_cve_report::parse(root, workspace_id, path, report, digest); } @@ -2150,6 +2159,13 @@ fn render_blocker_task(report: &ReportInput, blocker: &BlockerInput) -> String { blocker.id, blocker.reason, report.digest, recheck, report.run_id ); } + if blocker.checker_id == "python.ruff" && blocker.reason == "python_syntax_confirmation_needed" + { + return format!( + "# {} Python 语法原生确认任务\n\n- 问题证据:源码范围 `{}` 的候选 WASM 初检尚未验收;脱敏疑似位置、源码及 grammar 摘要在 `.codeguard/reports/{}.json`,报告 SHA-256 `{}`。疑似位置不是已确认源码违规。\n- 规则依据:Tree-sitter 恢复节点只提示待核实位置,须用适用的 Python 原生语法能力和项目原配置确认。\n- 允许范围:只核对本源码、对应构建根、原生工具及配置;不得凭候选观察修改无关源码或增加白名单。\n- 修复步骤:查看同 run 的位置,再恢复原生检查;若原生反证,保留证据并调查 grammar 误报。\n- 复检命令:codeguard task verify {} . --ruff-tool <已核验绝对路径> --format json。\n- 历史尝试:首次 run `{}`;后续候选观察归并到同一任务。\n- 关闭条件:当前输入、范围与语法能力匹配的原生复检和可信策略核验完成;安装工具、任务勾选或后续 WASM 零恢复节点都不能关闭。\n", + blocker.id, blocker.scope, report.run_id, report.digest, blocker.id, report.run_id + ); + } let step = if blocker.reason == "project_ruff_config_not_found" { "确认项目是否要求 Ruff;若要求,按已批准规则配置 Ruff 后复扫;若不要求,修订项目质量策略。" } else if blocker.reason.starts_with("ruff_config_") || blocker.reason == "config_changed" { diff --git a/crates/codeguard-cli/src/work_sync/python_syntax_confirmation_report.rs b/crates/codeguard-cli/src/work_sync/python_syntax_confirmation_report.rs new file mode 100644 index 00000000..6d7ef077 --- /dev/null +++ b/crates/codeguard-cli/src/work_sync/python_syntax_confirmation_report.rs @@ -0,0 +1,46 @@ +//! Python 语法候选报告只生成原生确认阻塞,不导入源码违规。 + +use super::{BlockerInput, ReportInput}; +use serde_json::Value; +use std::path::Path; + +/// 校验固定身份与当前源码后,投影一张稳定的待确认任务。 +pub(super) fn parse( + root: &Path, + workspace: &str, + path: &Path, + report: &Value, + digest: String, +) -> Result { + if !crate::python_syntax_confirmation::valid_report(root, workspace, report) { + return Err("python_syntax_confirmation_report_invalid"); + } + let run = report["run_id"].as_str().ok_or("report_run_id_invalid")?; + if path.file_stem().and_then(|stem| stem.to_str()) != Some(run) { + return Err("report_run_id_invalid"); + } + let scope = report["scope"].as_str().ok_or("report_scope_invalid")?; + Ok(ReportInput { + workspace_id: workspace.into(), + run_id: run.into(), + digest, + findings: Vec::new(), + historical_findings: 0, + blockers: vec![BlockerInput { + checker_id: "python.ruff".into(), + id: report["blocker_id"] + .as_str() + .ok_or("blocker_id_invalid")? + .into(), + fingerprint: report["fingerprint"] + .as_str() + .ok_or("fingerprint_invalid")? + .into(), + reason: "python_syntax_confirmation_needed".into(), + diagnostic_reason: Some("python_syntax_confirmation_needed".into()), + build_root: ".".into(), + scope: scope.into(), + affected_paths: vec![scope.into()], + }], + }) +} diff --git a/crates/codeguard-cli/tests/lint_python_cli.rs b/crates/codeguard-cli/tests/lint_python_cli.rs index f86daee8..4b23987c 100644 --- a/crates/codeguard-cli/tests/lint_python_cli.rs +++ b/crates/codeguard-cli/tests/lint_python_cli.rs @@ -393,7 +393,7 @@ fn initialized_lint_report_binds_the_existing_workspace_identity() { .unwrap(); let (_, report) = run(&project, &["--ruff-tool", "/nonexistent/ruff"]); #[cfg(feature = "wasm-precheck")] - assert_eq!(report["schema_version"], "0.14.0"); + assert_eq!(report["schema_version"], "0.15.0"); #[cfg(not(feature = "wasm-precheck"))] assert_eq!(report["schema_version"], "0.13.0"); assert_eq!(report["execution_budget"]["timeout_ms"], 1_800_000); diff --git a/crates/codeguard-cli/tests/python_syntax_fallback_candidate.rs b/crates/codeguard-cli/tests/python_syntax_fallback_candidate.rs index 2f07ed18..4dae0ccc 100644 --- a/crates/codeguard-cli/tests/python_syntax_fallback_candidate.rs +++ b/crates/codeguard-cli/tests/python_syntax_fallback_candidate.rs @@ -135,6 +135,205 @@ fn invalid_ruff_config_does_not_get_laundered_by_wasm_precheck() { assert_eq!(report["delivery_decision"], "not_evaluated"); } +#[test] +fn initialized_workspace_keeps_one_python_native_confirmation_task_across_rescans() { + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + let source = root.join("broken.py"); + fs::write(&source, "def broken(\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 = project.lint(&["--file", "broken.py"]); + assert_eq!(first["schema_version"], "0.15.0"); + assert_eq!(first["task_persistence"]["status"], "synced_partial"); + let task_id = first["setup"]["task_id"].as_str().expect("真实任务 ID"); + let task_path = root.join(".codeguard/tasks").join(format!("{task_id}.md")); + assert!(task_path.is_file()); + let fact_path = root + .join(".codeguard/findings") + .join(task_id) + .join("finding.json"); + let fact: Value = serde_json::from_slice(&fs::read(&fact_path).unwrap()).unwrap(); + assert_eq!(fact["checker_id"], "python.ruff"); + assert_eq!(fact["reason_code"], "python_syntax_confirmation_needed"); + assert_eq!(fact["state"], "open"); + let task_text = fs::read_to_string(&task_path).unwrap(); + assert!(task_text.contains("Python 语法原生确认任务")); + assert!(task_text.contains("原生复检")); + let confirmation: Value = fs::read_dir(root.join(".codeguard/reports")) + .unwrap() + .filter_map(|entry| { + let path = entry.ok()?.path(); + serde_json::from_slice::(&fs::read(path).ok()?).ok() + }) + .find(|report| report["report_type"] == "python_syntax_confirmation_observation") + .unwrap(); + assert_eq!(confirmation["blocker_id"], task_id); + assert_eq!(confirmation["scope"], "broken.py"); + assert_eq!( + confirmation["observations"][0]["classification"], + "suspected" + ); + assert!(!confirmation.to_string().contains("def broken")); + let second = project.lint(&["--file", "broken.py"]); + assert_eq!(second["setup"]["task_id"], task_id); + fs::write(&source, "def repaired():\n return 1\n").unwrap(); + let changed = project.lint(&["--file", "broken.py"]); + assert_eq!(changed["setup"]["task_id"], task_id); + assert!( + changed["syntax_precheck"]["observations"] + .as_array() + .unwrap() + .is_empty() + ); + let fact: Value = serde_json::from_slice(&fs::read(&fact_path).unwrap()).unwrap(); + assert_eq!(fact["state"], "open"); + assert_eq!(changed["delivery_decision"], "not_evaluated"); +} + +#[test] +fn forged_python_syntax_task_evidence_cannot_be_imported() { + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + fs::write(root.join("broken.py"), "def broken(\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 = project.lint(&["--file", "broken.py"]); + let task_id = first["setup"]["task_id"].as_str().unwrap(); + let report_dir = root.join(".codeguard/reports"); + let valid: Value = fs::read_dir(&report_dir) + .unwrap() + .filter_map(|entry| { + serde_json::from_slice::(&fs::read(entry.ok()?.path()).ok()?).ok() + }) + .find(|report| report["report_type"] == "python_syntax_confirmation_observation") + .unwrap(); + let mut wrong_grammar = valid.clone(); + wrong_grammar["run_id"] = Value::String("python-syntax-999-1".into()); + wrong_grammar["grammar_sha256"] = Value::String("0".repeat(64)); + fs::write( + report_dir.join("python-syntax-999-1.json"), + serde_json::to_vec_pretty(&wrong_grammar).unwrap(), + ) + .unwrap(); + let mut out_of_bounds = valid; + out_of_bounds["run_id"] = Value::String("python-syntax-999-2".into()); + out_of_bounds["observations"][0]["start_line"] = Value::from(999); + fs::write( + report_dir.join("python-syntax-999-2.json"), + serde_json::to_vec_pretty(&out_of_bounds).unwrap(), + ) + .unwrap(); + let mut duplicate = wrong_grammar; + duplicate["run_id"] = Value::String("python-syntax-999-3".into()); + duplicate["grammar_sha256"] = first["syntax_precheck"]["grammar_sha256"].clone(); + let encoded = serde_json::to_string(&duplicate).unwrap(); + let repeated = encoded.replacen( + "\"grammar_sha256\":", + "\"grammar_sha256\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"grammar_sha256\":", + 1, + ); + fs::write(report_dir.join("python-syntax-999-3.json"), repeated).unwrap(); + let sync = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args(["work", "sync", root.to_str().unwrap(), "--format=json"]) + .output() + .unwrap(); + assert_eq!(sync.status.code(), Some(3)); + let result: Value = serde_json::from_slice(&sync.stdout).unwrap(); + assert_eq!(result["failed_reports"], 3, "{result}"); + assert_eq!(result["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 failed_task_persistence_is_visible_without_a_fabricated_task_id() { + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + fs::write(root.join("broken.py"), "def broken(\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 reports = root.join(".codeguard/reports"); + fs::remove_dir(&reports).unwrap(); + fs::write(&reports, b"occupied").unwrap(); + let report = project.lint(&["--file", "broken.py"]); + assert_eq!(report["schema_version"], "0.15.0"); + assert_eq!(report["task_persistence"]["status"], "unavailable"); + assert_eq!( + report["task_persistence"]["reason"], + "reports_directory_unavailable" + ); + assert!(report["setup"]["task_id"].is_null()); + assert_eq!(report["syntax_precheck"]["checked_files"], 1); + assert_eq!(report["delivery_decision"], "not_evaluated"); +} + +#[test] +#[ignore = "requires native Ruff 0.16.8 via CODEGUARD_RUFF_BIN"] +fn native_ruff_task_verify_records_an_attempt_without_closing_candidate_task() { + let tool = std::env::var("CODEGUARD_RUFF_BIN").unwrap(); + let project = Project::new(); + let root = project.0.canonicalize().unwrap(); + fs::write(root.join("ruff.toml"), "[lint]\nselect = ['F401']\n").unwrap(); + fs::write(root.join("broken.py"), "def broken(\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 = project.lint(&["--file", "broken.py"]); + let id = first["setup"]["task_id"].as_str().unwrap(); + let verify = Command::new(env!("CARGO_BIN_EXE_codeguard")) + .args([ + "task", + "verify", + id, + root.to_str().unwrap(), + "--ruff-tool", + &tool, + "--format=json", + ]) + .output() + .unwrap(); + assert_eq!( + verify.status.code(), + Some(3), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); + let report: Value = serde_json::from_slice(&verify.stdout).unwrap(); + assert_eq!(report["task_id"], id); + assert_eq!(report["native_scan"]["files"][0]["path"], "broken.py"); + assert_eq!(report["delivery_decision"], "not_evaluated"); + let fact: Value = serde_json::from_slice( + &fs::read( + root.join(".codeguard/findings") + .join(id) + .join("finding.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(fact["state"], "open"); +} + #[test] fn oversized_sibling_remains_visible_as_unavailable_without_erasing_other_suspicions() { let project = Project::new(); diff --git a/docs/Codeguard-Architecture.md b/docs/Codeguard-Architecture.md index 65299efd..942437a1 100644 --- a/docs/Codeguard-Architecture.md +++ b/docs/Codeguard-Architecture.md @@ -241,7 +241,7 @@ A completed native violation never selects fallback to obtain a clean result. A Reuse pinned CodeGraph grammar WASM artifacts and applicable upstream licenses, source references, patches and corpus cases. Do not copy CodeGraph's graph extraction or source-blanking heuristics into a syntax verdict. CodeGraph's `src/extraction/grammars.ts` loads language artifacts through `web-tree-sitter`; successful loading there does not prove compatibility with Codeguard's Rust runtime. The local source review on 2026-09-28 found modified grammar artifacts and about 65 MB of grammar files, so a live directory is not a release input. -The read-only `codeguard grammar status` command now projects a [pinned CodeGraph coverage inventory](../grammars/codegraph-coverage.json): 30 vendored WASM files and two dependency-provided grammars, with Java, Python, TypeScript and TSX as CodeGuard asset candidates and none qualified for release. Python has a narrow source-build-only fallback for unavailable or undeclared Ruff, with native results taking priority and no delivery approval. This inventory is deliberately separate from the pinned [candidate asset manifest](../grammars/manifest.json). In particular, the COBOL source file exceeds the current loader's 8 MiB input limit. Inventory rows never trigger loading or count as supported parser capability. +The read-only `codeguard grammar status` command now projects a [pinned CodeGraph coverage inventory](../grammars/codegraph-coverage.json): 30 vendored WASM files and two dependency-provided grammars, with Java, Python, TypeScript and TSX as CodeGuard asset candidates and none qualified for release. Python has a narrow source-build-only fallback for unavailable or undeclared Ruff, with native results taking priority and no delivery approval. An initialized single-file scope now syncs one stable native-confirmation task; candidate zero recoveries do not close it. This inventory is deliberately separate from the pinned [candidate asset manifest](../grammars/manifest.json). In particular, the COBOL source file exceeds the current loader's 8 MiB input limit. Inventory rows never trigger loading or count as supported parser capability. ```text grammars/ # proposed release assets, not project state diff --git a/docs/Codeguard-Architecture.zh_CN.md b/docs/Codeguard-Architecture.zh_CN.md index 553672d8..24a37d59 100644 --- a/docs/Codeguard-Architecture.zh_CN.md +++ b/docs/Codeguard-Architecture.zh_CN.md @@ -241,7 +241,7 @@ flowchart TD 复用固定版本的 CodeGraph grammar WASM,以及适用的上游许可证、源码引用、补丁和语料。不能把 CodeGraph 的图提取或源码遮盖启发式直接用于语法判定。CodeGraph 的 `src/extraction/grammars.ts` 通过 `web-tree-sitter` 加载语言资产,在那里可加载不证明兼容 Codeguard 的 Rust 运行时。发行时必须固定不可变的来源与资产清单,不能把正在修改的本地目录直接当作发行输入。 -只读 `codeguard grammar status` 现投影[固定来源覆盖库存](../grammars/codegraph-coverage.json):CodeGraph 随仓 30 份 WASM,依赖提供另两种独立 grammar;CodeGuard 有 Java、Python、TypeScript、TSX 四份资产候选,尚无已验收发行能力。Python 已有源码可选构建中的局部 `lint` 兜底,仅覆盖 Ruff 不可用或项目未声明配置的情形;原生结果优先且不能批准交付。覆盖库存与[候选资产清单](../grammars/manifest.json)分开,不能凭库存行自动加载或宣称支持。尤其 COBOL 来源资产超过当前加载器 8 MiB 限额。 +只读 `codeguard grammar status` 现投影[固定来源覆盖库存](../grammars/codegraph-coverage.json):CodeGraph 随仓 30 份 WASM,依赖提供另两种独立 grammar;CodeGuard 有 Java、Python、TypeScript、TSX 四份资产候选,尚无已验收发行能力。Python 已有源码可选构建中的局部 `lint` 兜底,仅覆盖 Ruff 不可用或项目未声明配置的情形;原生结果优先且不能批准交付。已初始化工作区的单文件范围可同步稳定的原生确认任务;候选零恢复节点不会关闭任务。覆盖库存与[候选资产清单](../grammars/manifest.json)分开,不能凭库存行自动加载或宣称支持。尤其 COBOL 来源资产超过当前加载器 8 MiB 限额。 ```text grammars/ # 规划中的发行资产,不是项目状态 diff --git a/docs/Codeguard-Command-Reference.md b/docs/Codeguard-Command-Reference.md index c92b7c8e..3010698f 100644 --- a/docs/Codeguard-Command-Reference.md +++ b/docs/Codeguard-Command-Reference.md @@ -16,7 +16,7 @@ With `--workspace` pointing to an initialized, canonical workspace, the TypeScri `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). -A separate Python execution slice now accepts `codeguard lint python . --file src/changed.py --format json`. Repeatable `--file` values share the eight-file, 512-byte per-path and 2-KiB combined budget. Only precisely selected, discovered Python files are scanned; an undiscovered target remains incomplete. The default CLI conversation response is `python_lint_feedback` 0.13 with `scan_scope=selected_files` and `requested_paths`. An opt-in source build may return 0.14 with bounded Python WASM suspicion when Ruff is unavailable or undeclared; native feedback and `delivery_decision=not_evaluated` remain. The selected-file response is not imported as a full workbench scan. The previous 0.12 schema remains for historical results; the nested Python result in `check all` has not been upgraded. `hook plan` does not yet invoke this command automatically; other languages, hosts and bounded batch execution remain unwired. +A separate Python execution slice now accepts `codeguard lint python . --file src/changed.py --format json`. Repeatable `--file` values share the eight-file, 512-byte per-path and 2-KiB combined budget. Only precisely selected, discovered Python files are scanned; an undiscovered target remains incomplete. The default CLI conversation response is `python_lint_feedback` 0.13 with `scan_scope=selected_files` and `requested_paths`. An opt-in source build may return 0.14 with bounded Python WASM suspicion when Ruff is unavailable or undeclared. For one checked file in an initialized workspace it returns 0.15 with a real stable native-confirmation task ID after successful sync, or an explicit persistence failure and null task ID. Native feedback and `delivery_decision=not_evaluated` remain. The selected-file response is not imported as a full workbench scan. The previous 0.12 schema remains for historical results; the nested Python result in `check all` has not been upgraded. `hook plan` does not yet invoke this command automatically; other languages, hosts and bounded batch execution remain unwired. The current partial execution entry is `codeguard hook execute PATH --timeout DURATION --format=json [--ruff-tool ABS_PATH] [--git-tool ABS_PATH]`, reading the same 1.0.0 `hook_trigger_request` from stdin. The explicit timeout is capped at 120 seconds. Session start only discovers; Stop reads at most 64 findings and 64 reports with an 8 MiB report-byte budget and returns `hook_next_guidance` without running a checker. An oversized backlog returns `not_run/guidance_scope_exceeded`; use `codeguard next PATH --format=json` explicitly. An empty task list recommends a fresh full check, never a pass. `repair_ready` first bounds one task history to 128 events and 1 MiB, then verifies an existing stable task through the same `task verify` implementation in a bounded child process; a missing task, oversized history or failed child remains incomplete. Only task/checker identity, observation, saved-event state and a bounded reason are projected back, while the full native report stays in the existing local verification path. Explicit tool options supported by `task verify` may be passed to `hook execute`; they are rejected on unrelated events. A confirmed Python-only edit runs local Ruff. With an explicit Git binary, `pre_commit` reads the current staged index, including an alternate `GIT_INDEX_FILE`; the Git observation itself is bounded by the lesser of the supplied timeout and 15 seconds. Its `hook_git_index_summary` reports index identity, total violation count, at most 32 paths of at most 512 bytes each, truncation and object status while `source_check=not_run`; it cannot prove full quality coverage. Missing Git or failed index observation is `not_run`. Failed or uncertain writes, mixed languages, push and CI also remain `not_run`. The outer `hook_execution_feedback` is now 0.5; historical 0.1–0.4 schemas are preserved. `prompt_submitted` returns constant `read_only_intent_guidance` with no source check or delivery verdict. `host_blocking_verified=false` and `delivery_decision=not_evaluated` remain fixed even if the host claims blocking. No plugin Hook invokes this entry automatically yet; it is not a strict gate. diff --git a/docs/Codeguard-Command-Reference.zh_CN.md b/docs/Codeguard-Command-Reference.zh_CN.md index b703a0df..a259f7ef 100644 --- a/docs/Codeguard-Command-Reference.zh_CN.md +++ b/docs/Codeguard-Command-Reference.zh_CN.md @@ -419,7 +419,7 @@ gate 与 check 使用同一个义务/适配器/报告内核,差异是权威内 当前已有局部只读入口 `codeguard hook plan [--format=json]`,从 stdin 读取最多 64 KiB 的 [`hook_trigger_request` 1.0.0](../schemas/hook-trigger-request.schema.json),返回 [`hook_trigger_plan` 1.1.0](../schemas/hook-trigger-plan-v1.1.schema.json) 与退出码 3。确认成功的编辑最多选择 8 个不同路径、单路径 512 字节及总计 2 KiB 作逐文件快检;超预算返回 `fast_scope_budget_exceeded` 并要求重新确定有界批量范围,不截断后假称已检查,也不能反复提交同一超预算列表。旧 [1.0 响应 schema](../schemas/hook-trigger-plan.schema.json)保留供历史消费者识别。`execution=not_run`、`delivery_decision=not_evaluated` 固定,尚未调用检查器、取得 Git 快照或阻断任一宿主。它不是下述完整宿主接入的验收证明,见[局部验收](../tests/acceptance/hook-plan-cli-candidate.md)。 -已提供独立的 Python 执行切片:`codeguard lint python . --file src/changed.py --format json`。`--file` 可重复,但遵守同一 8 文件/512 字节单路径/2 KiB 总路径预算;只扫描被发现且精确选中的 Python 文件,未发现目标保留未完成原因。默认 CLI 对话报告为 `python_lint_feedback` 0.13,返回 `scan_scope=selected_files` 和 `requested_paths`。源码可选构建在 Ruff 不可用或未声明时可返回 0.14 的有界 Python WASM 疑似位置,保留原生反馈和 `delivery_decision=not_evaluated`;局部反馈不作为完整工作台扫描导入。原 0.12 报告 schema 保留供历史结果识别;`check all` 的嵌套 Python 结果未因此升级。目前 `hook plan` 尚未自动调用它;其它语言、宿主入口和批量范围执行仍待接线。 +已提供独立的 Python 执行切片:`codeguard lint python . --file src/changed.py --format json`。`--file` 可重复,但遵守同一 8 文件/512 字节单路径/2 KiB 总路径预算;只扫描被发现且精确选中的 Python 文件,未发现目标保留未完成原因。默认 CLI 对话报告为 `python_lint_feedback` 0.13,返回 `scan_scope=selected_files` 和 `requested_paths`。源码可选构建在 Ruff 不可用或未声明时可返回 0.14 的有界 Python WASM 疑似位置;已初始化工作区的单文件范围同步成功后返回 0.15 的真实稳定原生确认任务 ID,失败则显示持久化原因且任务 ID 为空。原生反馈和 `delivery_decision=not_evaluated` 保留;局部反馈不作为完整工作台扫描导入。原 0.12 报告 schema 保留供历史结果识别;`check all` 的嵌套 Python 结果未因此升级。目前 `hook plan` 尚未自动调用它;其它语言、宿主入口和批量范围执行仍待接线。 现有局部执行入口 `codeguard hook execute PATH --timeout DURATION --format=json [--ruff-tool ABS_PATH] [--git-tool ABS_PATH]` 从 stdin 接收同一 `hook_trigger_request` 1.0.0。超时必须显式提供且不超过 120s。启动事件只读发现;Stop 只读最多 64 个 finding 和 64 份报告,报告字节总预算为 8 MiB,返回不调用检查器的 `hook_next_guidance`。记录超预算返回 `not_run/guidance_scope_exceeded`,需要显式执行 `codeguard next PATH --format=json`;无任务时要求新鲜完整检查,不能视作通过。`repair_ready` 先将单任务历史限制在 128 项和 1 MiB,再按稳定任务 ID 复用现有 `task verify`,通过有界子进程按原检查器复检;任务缺失、历史超限或子进程失败保留未完成。对话仅返回任务/检查器身份、观察结果、事件是否保存及有界原因,原生完整报告仍在既有本地复检路径中。`task verify` 的显式工具参数可传给 `hook execute`,其它事件收到这些参数会拒绝。确认成功且目标全为 Python 的编辑事件执行局部 Ruff。显式给出 Git 二进制后,`pre_commit` 读取本轮真实暂存 index(含替代 `GIT_INDEX_FILE`),Git 观察本身的截止时间取传入预算与 15s 较小值;`hook_git_index_summary` 给出 index 身份、违规总数、最多 32 个且每个至多 512 字节的路径、截断状态与对象状态,同时 `source_check=not_run`,不证明完整质量覆盖。Git 缺失或 index 观察失败仍为 `not_run`。失败/未知写入、混合语言、推送和 CI 仍保持 `not_run`。外层 `hook_execution_feedback` 升为 0.5,历史 0.1–0.4 schema 保留;`prompt_submitted` 仅返回固定的 `read_only_intent_guidance`,不运行源码检查、不签发交付结论;即使宿主声称可阻断,也固定 `host_blocking_verified=false`、`delivery_decision=not_evaluated`。该命令还没有被插件 Hook 自动调用,不能代替严格门禁。 diff --git a/docs/Codeguard-Technical-Design.md b/docs/Codeguard-Technical-Design.md index 719691ba..c72e7fac 100644 --- a/docs/Codeguard-Technical-Design.md +++ b/docs/Codeguard-Technical-Design.md @@ -191,7 +191,7 @@ The opt-in CLI build first checks for a local ESLint 10 package and a single ord Import only pinned WASM bytes with upstream license, commit/patch provenance, SHA-256, ABI, tested runtime and language/dialect ranges, corpus reference and known gaps. Verify each copied CodeGraph artifact in the selected Rust Tree-sitter runtime; do not copy TypeScript extraction logic or assume CodeGraph support equals syntax acceptance. A release manifest belongs to distribution assets, not writable `.codeguard/` project state. -Use `codeguard grammar status --format=json` to query the separate source coverage inventory. It reports 32 distinct CodeGraph grammar assets, four CodeGuard asset candidates, and zero released capabilities without running a parser. Python has a narrow source-build-only `lint` fallback for unavailable or undeclared Ruff, never a clean or delivery result. The two dependency-provided assets lack pinned bytes, and COBOL exceeds the current 8 MiB loader limit; these remain explicit gaps before any per-language qualification. +Use `codeguard grammar status --format=json` to query the separate source coverage inventory. It reports 32 distinct CodeGraph grammar assets, four CodeGuard asset candidates, and zero released capabilities without running a parser. Python has a narrow source-build-only `lint` fallback for unavailable or undeclared Ruff, never a clean or delivery result. A bound, single-file result uses [feedback 0.15.0](../schemas/python-lint-feedback-v0.15.schema.json) and persists a [0.1.0 confirmation observation](../schemas/python-syntax-confirmation-observation-v0.1.schema.json) through the existing work sync. The task identity binds workspace and source scope; current-source SHA-256 and pinned grammar are checked before import. Native capability-matched closure remains open. The two dependency-provided assets lack pinned bytes, and COBOL exceeds the current 8 MiB loader limit; these remain explicit gaps before any per-language qualification. Use a parent-controlled parser worker with a total deadline, per-file input bound, memory/process limit and capped diagnostics. Lazy-load selected grammars, parse without general network/filesystem imports, and terminate a stuck worker without losing native results from other modules. Host-specific enforcement and Rust MSRV compatibility must be tested; numeric production budgets remain measurement-driven rather than invented promises. diff --git a/docs/Codeguard-Technical-Design.zh_CN.md b/docs/Codeguard-Technical-Design.zh_CN.md index f1e4f729..3c71259d 100644 --- a/docs/Codeguard-Technical-Design.zh_CN.md +++ b/docs/Codeguard-Technical-Design.zh_CN.md @@ -191,7 +191,7 @@ flowchart LR 只引入固定 WASM 字节及上游许可证、提交/补丁来源、SHA-256、ABI、已验证运行时和语言/方言范围、语料引用与已知缺口。逐个验证 CodeGraph 资产与选定 Rust Tree-sitter 运行时兼容,不能复制 TypeScript 提取逻辑或把 CodeGraph 支持等同语法验收。发行清单属于分发资产,不属于可写的 `.codeguard/` 项目状态。 -可用 `codeguard grammar status --format=json` 查询独立的来源覆盖库存。它只读报告 CodeGraph 32 种独立 grammar、CodeGuard 四份资产候选和零项已发行能力,不运行解析器;Python 已有源码可选构建中的局部 `lint` 兜底,结果不能视为通过。依赖提供的两份资产尚未固定字节,COBOL 超过现有加载器的 8 MiB 限额;每语言验收前仍须解决这些缺口。 +可用 `codeguard grammar status --format=json` 查询独立的来源覆盖库存。它只读报告 CodeGraph 32 种独立 grammar、CodeGuard 四份资产候选和零项已发行能力,不运行解析器;Python 已有源码可选构建中的局部 `lint` 兜底,结果不能视为通过。已绑定工作区的单文件反馈使用 [0.15.0 协议](../schemas/python-lint-feedback-v0.15.schema.json),并通过现有 work sync 保存 [0.1.0 确认观察](../schemas/python-syntax-confirmation-observation-v0.1.schema.json)。任务身份绑定工作区和源码范围;导入前核对当前源码 SHA-256 与固定 grammar。能力匹配的原生关闭仍未完成。依赖提供的两份资产尚未固定字节,COBOL 超过现有加载器的 8 MiB 限额;每语言验收前仍须解决这些缺口。 由父进程控制解析工作进程,设置总截止时间、单文件输入上限、内存/进程限制及诊断数量上限。按需加载 grammar,不提供通用网络/文件系统导入;终止卡住的进程时保留其它模块的原生结果。宿主约束执行和 Rust MSRV 兼容性须测试,生产数值预算须经测量后确定,不编造性能承诺。 diff --git a/openspec/changes/introduce-rust-codeguard-cli/tasks.md b/openspec/changes/introduce-rust-codeguard-cli/tasks.md index 186c77cf..0d180898 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 与混合结果保留仍缺,不勾选。 Python `lint` 现先保留 Ruff 原生结果;工具缺失或项目未声明 Ruff 时,对有界文件附加 WASM 疑似位置。无效配置与已有原生诊断不被 WASM 洗白;见[Python 兜底局部验收](../../../tests/acceptance/python-syntax-fallback-candidate.md)。 -- [ ] 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 拒绝与发布兼容仍缺,不勾选。 新增 Python 0.14.0 局部反馈 schema,明确原生状态、候选范围、源码和 grammar SHA-256、缺口与疑似位置;零观察仍 incomplete,见[Python 兜底局部验收](../../../tests/acceptance/python-syntax-fallback-candidate.md)。 +- [ ] 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 拒绝与发布兼容仍缺,不勾选。 新增 Python 0.14.0 局部反馈 schema,明确原生状态、候选范围、源码和 grammar SHA-256、缺口与疑似位置;零观察仍 incomplete,见[Python 兜底局部验收](../../../tests/acceptance/python-syntax-fallback-candidate.md)。 Python 已初始化单文件反馈升级至 0.15.0,独立 0.1.0 脱敏确认报告绑定源码、固定 grammar 和真实任务引用;同步失败保留空 ID 与原因,见[局部验收](../../../tests/acceptance/python-syntax-fallback-candidate.md)。 - [ ] 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) 局部验收。正式四类报告、多模块范围与宿主渲染仍缺,不勾选。 Python 0.14.0 JSON/human 局部反馈现区分 Ruff 缺失、配置未声明、WASM 疑似及不可用范围,始终返回 incomplete、退出码 3 且任务 ID 不伪造;见[局部验收](../../../tests/acceptance/python-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)。TypeScript/TSX 有界疑似位置已用源码/grammar 身份保存到 0.2.0 准备报告并由任务报告摘要引用,导入拒绝伪造 grammar 和越界坐标;失败尝试/无进展预算、推荐项非阻断投影、Java 与多模块任务尚缺,不勾选。 +- [ ] 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) 局部验收。正式四类报告、多模块范围与宿主渲染仍缺,不勾选。 Python 0.14.0 JSON/human 局部反馈现区分 Ruff 缺失、配置未声明、WASM 疑似及不可用范围,始终返回 incomplete、退出码 3 且任务 ID 不伪造;见[局部验收](../../../tests/acceptance/python-syntax-fallback-candidate.md)。 Python 已初始化单文件结果只在实际同步成功后返回稳定 `setup.task_id`;0.15.0 区分持久化成功/失败,未初始化仍是 0.14.0 和空 ID,见[局部验收](../../../tests/acceptance/python-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)。TypeScript/TSX 有界疑似位置已用源码/grammar 身份保存到 0.2.0 准备报告并由任务报告摘要引用,导入拒绝伪造 grammar 和越界坐标;失败尝试/无进展预算、推荐项非阻断投影、Java 与多模块任务尚缺,不勾选。 Python 单文件候选观察已接入既有 work sync,工作区/源码范围稳定归并;反复扫描及后续 WASM 零恢复节点不关闭任务。导入拒绝伪造 grammar 与越界位置;多文件任务和能力匹配关闭仍缺,见[局部验收](../../../tests/acceptance/python-syntax-fallback-candidate.md)。 - [ ] 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/openspec/changes/introduce-rust-codeguard-cli/verification.md b/openspec/changes/introduce-rust-codeguard-cli/verification.md index 0a484524..cb38cad0 100644 --- a/openspec/changes/introduce-rust-codeguard-cli/verification.md +++ b/openspec/changes/introduce-rust-codeguard-cli/verification.md @@ -2078,3 +2078,9 @@ TDD 先复现报告漏洞:把源码 finding 的主定位、原生身份及白 ## 2026-09-29 暂存对象部分失败保留发现 `git_index_safety` 以前在任一暂存 blob 超过读取预算时丢弃所有内容观察。真实 Git 混合样本先红后绿:超 8 MiB 对象单独 unresolved,同一 index 中已核对 OID 的 `.codeguard/findings` 私钥 finding 仍出现在本地与公开 JSON;执行保持 3/incomplete/not_evaluated。受控 Git 65 个不同 OID 的第二批坏响应证明前 64 个已独立核对证据保留,第 65 个未完成。目标 Git 契约 17/17 通过,宿主事件契约 18 通过、2 项因本机 Ruff 条件跳过;CLI Clippy `-D warnings`、fmt 与 OpenSpec strict 通过。[PR #6](https://github.com/full-stack-plugins/codeguard/pull/6) 的最终 [CI 36518917839](https://github.com/full-stack-plugins/codeguard/actions/runs/36518917839) 全部通过,合并提交 `0940b028cfdd4d71a3c77390bea9a7f4f10b0d8a`。完整快照、其它坏响应组合、正式 Git/CI 门禁和跨平台验收仍缺,2.3/3.5/11.5 不勾选。 + +## 2026-09-29 Python WASM 候选原生确认任务 + +已初始化工作区的单文件 `lint python` 候选报告使用 0.15.0 对话协议,将固定 Python grammar、当前源码摘要与脱敏疑似位置写入独立 0.1.0 本地报告,并经现有 work sync 形成按工作区和源码范围稳定归并的 `python_syntax_confirmation_needed` 阻塞任务。未初始化工作区继续使用 0.14.0 且任务 ID 为空。重扫同一源码及源码改为零恢复节点后,任务仍为 `open`;报告目录不可用时,对话保留候选结果,标明持久化原因而不伪造任务引用。导入拒绝 grammar 摘要篡改、越界坐标和重复 JSON 键;报告不含源码文本。 + +开发验证:新增 Python 端到端 8/8、相关 Python 原生路径 16/16(5 项真实工具用例默认忽略)、TypeScript 候选回归 14/14;本机 Ruff 0.16.8 的额外原生优先用例及 `task verify` 用例分别通过,后者记录观察而不关闭任务。实际 0.15.0 与本地 0.1.0 报告通过 JSON Schema,伪造同步成功状态被拒绝。先构建特性版产生报告,再用默认二进制执行 `work sync`,`failed_reports=0`。默认工作区测试通过,特性版 Clippy `-D warnings`、格式、分层检查及 OpenSpec strict 通过。能力匹配的原生复检关闭、多文件任务和真实宿主对话仍未完成,14.10 及相关总任务不勾选。 diff --git a/schemas/python-lint-feedback-v0.15.schema.json b/schemas/python-lint-feedback-v0.15.schema.json new file mode 100644 index 00000000..34128041 --- /dev/null +++ b/schemas/python-lint-feedback-v0.15.schema.json @@ -0,0 +1,949 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:codeguard:schema:python-lint-feedback:0.15.0", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "report_type", + "scope", + "scan_scope", + "requested_paths", + "operation", + "language", + "run_id", + "workspace_binding", + "workspace_id", + "checker_configurations", + "files", + "incomplete_reasons", + "local_scan_complete", + "command_status", + "exit_code", + "tool_approval", + "native_tool_version", + "rulepack_approval", + "adapter_sha256", + "backlog_status", + "backlog_sync", + "repair_brief_status", + "repair_brief_reason", + "next", + "delivery_decision", + "execution_budget", + "native", + "setup", + "next_action", + "syntax_precheck", + "task_persistence" + ], + "properties": { + "schema_version": { + "const": "0.15.0" + }, + "execution_budget": { + "type": "object", + "additionalProperties": false, + "required": [ + "timeout_ms", + "source", + "enforcement" + ], + "properties": { + "timeout_ms": { + "type": "integer", + "minimum": 1, + "maximum": 86400000 + }, + "source": { + "enum": [ + "cli", + "registered_environment", + "project_default", + "builtin_default" + ] + }, + "enforcement": { + "const": "native_and_bounded_syntax_worker" + } + } + }, + "report_type": { + "const": "python_lint_feedback" + }, + "scope": { + "const": "local_native_and_candidate_syntax_scan" + }, + "scan_scope": { + "enum": [ + "discovered_python", + "selected_files" + ] + }, + "requested_paths": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 1, + "maxItems": 8, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^(?!/)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]+$" + } + } + ] + }, + "operation": { + "const": "lint" + }, + "language": { + "const": "python" + }, + "run_id": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,120}$" + }, + "workspace_binding": { + "const": "bound" + }, + "workspace_id": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "pattern": "^ws-[0-9a-f]{32}$" + } + ] + }, + "checker_configurations": { + "type": "array", + "items": { + "type": "object" + } + }, + "files": { + "type": "array", + "items": { + "type": "object", + "required": [ + "run_status", + "tool_sha256", + "config_sha256", + "rule_settings", + "suppression_audit" + ], + "properties": { + "run_status": { + "enum": [ + "passed", + "findings", + "suppressed", + "incomplete" + ] + }, + "tool_sha256": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sha256" + } + ] + }, + "config_sha256": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sha256" + } + ] + }, + "rule_settings": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/rule_settings" + } + ] + }, + "suppression_audit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/suppression_audit" + } + ] + }, + "findings": { + "type": "array", + "items": { + "type": "object", + "required": [ + "codeguard_rule_id", + "rulepack_sha256", + "rulepack_status" + ], + "properties": { + "codeguard_rule_id": { + "type": [ + "string", + "null" + ] + }, + "rulepack_sha256": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sha256" + } + ] + }, + "rulepack_status": { + "enum": [ + "candidate_unapproved", + "unverified_incomplete", + "rulepack_unavailable", + "unmapped_or_unvalidated_version" + ] + } + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "run_status": { + "const": "incomplete" + } + } + }, + "then": { + "properties": { + "tool_sha256": { + "type": "null" + }, + "config_sha256": { + "type": "null" + }, + "rule_settings": { + "type": "null" + }, + "suppression_audit": { + "type": "null" + } + } + }, + "else": { + "properties": { + "tool_sha256": { + "$ref": "#/$defs/sha256" + }, + "config_sha256": { + "$ref": "#/$defs/sha256" + }, + "rule_settings": { + "$ref": "#/$defs/rule_settings" + }, + "suppression_audit": { + "$ref": "#/$defs/suppression_audit" + } + } + } + } + ] + } + }, + "incomplete_reasons": { + "type": "array", + "items": { + "type": "string" + } + }, + "local_scan_complete": { + "type": "boolean" + }, + "command_status": { + "enum": [ + "incomplete", + "cancelled" + ] + }, + "exit_code": { + "enum": [ + 3, + 130 + ] + }, + "tool_approval": { + "const": "unverified" + }, + "native_tool_version": { + "type": [ + "string", + "null" + ] + }, + "rulepack_approval": { + "const": "unverified" + }, + "adapter_sha256": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sha256" + } + ] + }, + "backlog_status": { + "enum": [ + "synced_partial", + "backlog_update_failed", + "not_initialized", + "not_synced_scoped" + ] + }, + "backlog_sync": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/backlog_sync" + } + ] + }, + "repair_brief_status": { + "enum": [ + "available", + "unavailable" + ] + }, + "repair_brief_reason": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1 + } + ] + }, + "next": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/next" + } + ] + }, + "delivery_decision": { + "const": "not_evaluated" + }, + "native": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "reason" + ], + "properties": { + "status": { + "const": "incomplete" + }, + "reason": { + "enum": [ + "ruff_tool_not_found", + "project_ruff_config_not_found", + "ruff_section_not_declared", + "ruff_lint_intent_not_explicit", + "ruff_config_not_resolved", + "multiple_native_incomplete_reasons" + ] + } + } + }, + "setup": { + "type": "object", + "additionalProperties": false, + "required": [ + "requirement", + "reason", + "task_id" + ], + "properties": { + "requirement": { + "const": "required" + }, + "reason": { + "const": "native_confirmation_needed" + }, + "task_id": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "pattern": "^CG-B-[0-9a-f]{32}$" + } + ] + } + } + }, + "next_action": { + "type": "string", + "minLength": 1 + }, + "syntax_precheck": { + "type": "object", + "additionalProperties": false, + "required": [ + "backend", + "language", + "status", + "reason", + "grammar_sha256", + "grammar_qualified", + "selected_files", + "checked_files", + "checked", + "unavailable", + "truncated", + "observations" + ], + "properties": { + "backend": { + "const": "bundled_tree_sitter_wasm_candidate" + }, + "language": { + "const": "python" + }, + "status": { + "const": "incomplete" + }, + "reason": { + "enum": [ + "partial_unavailable", + "recovery_or_file_budget_truncated", + "grammar_version_unqualified" + ] + }, + "grammar_sha256": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sha256" + } + ] + }, + "grammar_qualified": { + "const": false + }, + "selected_files": { + "type": "integer", + "minimum": 1 + }, + "checked_files": { + "type": "integer", + "minimum": 0, + "maximum": 8 + }, + "truncated": { + "type": "boolean" + }, + "checked": { + "type": "array", + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "source_sha256", + "grammar_sha256" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1 + }, + "source_sha256": { + "$ref": "#/$defs/sha256" + }, + "grammar_sha256": { + "$ref": "#/$defs/sha256" + } + } + } + }, + "unavailable": { + "type": "array", + "maxItems": 8, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "reason" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1 + }, + "reason": { + "enum": [ + "source_path_invalid", + "file_budget_exceeded", + "request_deadline_or_cancelled", + "source_unavailable", + "source_unavailable_or_over_budget", + "source_changed", + "grammar_or_worker_unavailable", + "worker_incomplete", + "grammar_identity_changed", + "position_mapping_incomplete" + ] + } + } + } + }, + "observations": { + "type": "array", + "maxItems": 128, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "classification", + "kind", + "syntax_kind", + "start_line", + "start_column", + "end_line", + "end_column", + "group_id", + "path", + "source_sha256" + ], + "properties": { + "classification": { + "const": "suspected" + }, + "kind": { + "enum": [ + "ERROR", + "MISSING" + ] + }, + "syntax_kind": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "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 + }, + "path": { + "type": "string", + "minLength": 1 + }, + "source_sha256": { + "$ref": "#/$defs/sha256" + } + } + } + } + } + }, + "task_persistence": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "reason" + ], + "properties": { + "status": { + "enum": [ + "synced_partial", + "unavailable" + ] + }, + "reason": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1, + "maxLength": 120 + } + ] + } + } + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "rule_settings": { + "type": "object", + "additionalProperties": false, + "required": [ + "settings_sha256", + "globally_enabled_mapped_rules", + "per_file_ignores_present", + "coverage_proven" + ], + "properties": { + "settings_sha256": { + "$ref": "#/$defs/sha256" + }, + "globally_enabled_mapped_rules": { + "type": "array", + "uniqueItems": true, + "items": { + "enum": [ + "E501", + "F401" + ] + } + }, + "per_file_ignores_present": { + "type": "boolean" + }, + "coverage_proven": { + "const": false + } + } + }, + "suppression_audit": { + "type": "object", + "additionalProperties": false, + "required": [ + "audit_sha256", + "suppressed_diagnostic_count", + "suppressed_rule_ids", + "scope" + ], + "properties": { + "audit_sha256": { + "$ref": "#/$defs/sha256" + }, + "suppressed_diagnostic_count": { + "type": "integer", + "minimum": 0 + }, + "suppressed_rule_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[A-Z0-9]{1,12}$" + } + }, + "scope": { + "const": "source_comments_only" + } + } + }, + "backlog_sync": { + "type": "object", + "additionalProperties": false, + "required": [ + "new_findings", + "new_blockers", + "imported_reports", + "historical_findings", + "failed_reports" + ], + "properties": { + "new_findings": { + "type": "integer", + "minimum": 0 + }, + "new_blockers": { + "type": "integer", + "minimum": 0 + }, + "imported_reports": { + "type": "integer", + "minimum": 0 + }, + "historical_findings": { + "type": "integer", + "minimum": 0 + }, + "failed_reports": { + "type": "integer", + "minimum": 0 + } + } + }, + "next": { + "type": "object", + "required": [ + "schema_version", + "report_type", + "disposition", + "repair_brief", + "authority", + "delivery_decision" + ], + "properties": { + "schema_version": { + "const": "0.1.0" + }, + "report_type": { + "const": "repair_brief_preview" + }, + "disposition": { + "enum": [ + "actionable", + "waiting", + "needs_decision", + "verification_required", + "no_applicable_work" + ] + }, + "repair_brief": { + "type": [ + "object", + "null" + ] + }, + "authority": { + "const": "local_unverified" + }, + "delivery_decision": { + "const": "not_evaluated" + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "scan_scope": { + "const": "selected_files" + } + }, + "required": [ + "scan_scope" + ] + }, + "then": { + "properties": { + "requested_paths": { + "type": "array" + }, + "backlog_status": { + "const": "not_synced_scoped" + }, + "backlog_sync": { + "type": "null" + }, + "repair_brief_status": { + "const": "unavailable" + }, + "repair_brief_reason": { + "const": "not_synced_scoped" + }, + "next": { + "type": "null" + } + } + }, + "else": { + "properties": { + "requested_paths": { + "type": "null" + }, + "backlog_status": { + "enum": [ + "synced_partial", + "backlog_update_failed", + "not_initialized" + ] + } + } + } + }, + { + "if": { + "properties": { + "command_status": { + "const": "cancelled" + } + }, + "required": [ + "command_status" + ] + }, + "then": { + "properties": { + "exit_code": { + "const": 130 + } + } + }, + "else": { + "properties": { + "exit_code": { + "const": 3 + } + } + } + }, + { + "if": { + "properties": { + "repair_brief_status": { + "const": "available" + } + }, + "required": [ + "repair_brief_status" + ] + }, + "then": { + "properties": { + "next": { + "$ref": "#/$defs/next" + }, + "repair_brief_reason": { + "type": "null" + } + } + } + }, + { + "if": { + "properties": { + "repair_brief_status": { + "const": "unavailable" + } + }, + "required": [ + "repair_brief_status" + ] + }, + "then": { + "properties": { + "next": { + "type": "null" + }, + "repair_brief_reason": { + "type": "string", + "minLength": 1 + } + } + } + }, + { + "if": { + "properties": { + "task_persistence": { + "properties": { + "status": { + "const": "synced_partial" + } + }, + "required": [ + "status" + ] + } + }, + "required": [ + "task_persistence" + ] + }, + "then": { + "properties": { + "setup": { + "properties": { + "task_id": { + "type": "string", + "pattern": "^CG-B-[0-9a-f]{32}$" + } + } + }, + "task_persistence": { + "properties": { + "reason": { + "type": "null" + } + } + } + } + }, + "else": { + "properties": { + "setup": { + "properties": { + "task_id": { + "type": "null" + } + } + }, + "task_persistence": { + "properties": { + "reason": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } + ] +} diff --git a/schemas/python-syntax-confirmation-observation-v0.1.schema.json b/schemas/python-syntax-confirmation-observation-v0.1.schema.json new file mode 100644 index 00000000..6bbc4cba --- /dev/null +++ b/schemas/python-syntax-confirmation-observation-v0.1.schema.json @@ -0,0 +1,164 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:codeguard:schema:python-syntax-confirmation-observation:0.1.0", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "report_type", + "workspace_binding", + "workspace_id", + "run_id", + "checker_id", + "authority", + "coverage_proven", + "delivery_decision", + "execution", + "reason_code", + "blocker_id", + "fingerprint", + "build_root", + "scope", + "affected_paths", + "source_sha256", + "grammar_sha256", + "observations" + ], + "properties": { + "schema_version": { + "const": "0.1.0" + }, + "report_type": { + "const": "python_syntax_confirmation_observation" + }, + "workspace_binding": { + "const": "bound" + }, + "workspace_id": { + "type": "string", + "pattern": "^ws-[0-9a-f]{32}$" + }, + "run_id": { + "type": "string", + "pattern": "^python-syntax-[0-9]+-[0-9]+$" + }, + "checker_id": { + "const": "python.ruff" + }, + "authority": { + "const": "local_unverified" + }, + "coverage_proven": { + "const": false + }, + "delivery_decision": { + "const": "not_evaluated" + }, + "execution": { + "const": "incomplete" + }, + "reason_code": { + "const": "python_syntax_confirmation_needed" + }, + "blocker_id": { + "type": "string", + "pattern": "^CG-B-[0-9a-f]{32}$" + }, + "fingerprint": { + "$ref": "#/$defs/sha256" + }, + "build_root": { + "const": "." + }, + "scope": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^(?!/)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]+\\.pyw?$" + }, + "affected_paths": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "source_sha256": { + "$ref": "#/$defs/sha256" + }, + "grammar_sha256": { + "$ref": "#/$defs/sha256" + }, + "observations": { + "type": "array", + "maxItems": 128, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "source_sha256", + "classification", + "kind", + "syntax_kind", + "start_line", + "start_column", + "end_line", + "end_column", + "group_id" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1 + }, + "source_sha256": { + "$ref": "#/$defs/sha256" + }, + "classification": { + "const": "suspected" + }, + "kind": { + "enum": [ + "ERROR", + "MISSING" + ] + }, + "syntax_kind": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "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": 0 + } + } + } + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + } + } +} diff --git a/tests/acceptance/python-syntax-fallback-candidate.md b/tests/acceptance/python-syntax-fallback-candidate.md index c918b16e..490bd231 100644 --- a/tests/acceptance/python-syntax-fallback-candidate.md +++ b/tests/acceptance/python-syntax-fallback-candidate.md @@ -1,6 +1,6 @@ # Python WASM 语法候选兜底:局部验收 -对应 OpenSpec `introduce-rust-codeguard-cli` 的 14.1、14.6、14.7、14.9 局部进展。此记录只适用于源码以 `wasm-precheck` 特性构建的 CLI;公开 npm `0.1.2` 未包含此能力。 +对应 OpenSpec `introduce-rust-codeguard-cli` 的 14.1、14.6、14.7、14.9、14.10 局部进展。此记录只适用于源码以 `wasm-precheck` 特性构建的 CLI;公开 npm `0.1.2` 未包含此能力。 `lint python` 先执行原有 Ruff 路径。只有选中文件的原生反馈为 `incomplete`、无原生 finding,且原因是 Ruff 工具缺失或项目未明确声明 Ruff 配置时,才由固定 Python grammar 和有界 Rust worker 补充疑似语法观察。无效 Ruff 配置保留原生阻塞;Ruff 已执行时不会用 WASM 覆盖诊断。最多处理八个文件、每文件 1 MiB、总计 128 个疑似位置,并沿用命令截止时间。超范围文件明确列在 `unavailable`。 @@ -26,7 +26,9 @@ JSON 使用 [Python 反馈 0.14.0](../../schemas/python-lint-feedback-v0.14.sche } ``` -此片段展示关键字段,完整报告另含源码/grammar SHA-256、位置、配置和同步状态。候选有位置不等于已证实源码违规;零位置也不等于 clean。`task_id: null` 表示尚未接通稳定 Python 原生确认任务,不能冒充已创建的任务。 +此片段展示未初始化工作区的关键字段,完整报告另含源码/grammar SHA-256、位置、配置和同步状态。候选有位置不等于已证实源码违规;零位置也不等于 clean。未初始化时 `task_id: null`,不能冒充已创建的任务。 + +已初始化工作区的单个已检查文件使用 [0.15.0 对话反馈](../../schemas/python-lint-feedback-v0.15.schema.json),同步成功后 `setup.task_id` 指向真实稳定任务;同步失败时 `task_persistence` 保留具体原因且任务 ID 为空。脱敏位置、源码和固定 grammar 摘要写入 [0.1.0 本地确认报告](../../schemas/python-syntax-confirmation-observation-v0.1.schema.json)。任务指向适用原生语法能力复检,不把恢复节点认作原生违规。 验证命令: @@ -34,6 +36,6 @@ JSON 使用 [Python 反馈 0.14.0](../../schemas/python-lint-feedback-v0.14.sche CARGO_NET_OFFLINE=true cargo test --locked -p codeguard-cli --features wasm-precheck --test python_syntax_fallback_candidate ``` -五个端到端反例覆盖配置的 Ruff 缺失、项目未声明 Ruff、有效语法、无效 Ruff 配置和超大兄弟文件;均保持退出码 3、`delivery_decision=not_evaluated`。真实 Ruff 优先路径另由 `lint_python_cli` 的忽略式本机用例核对。报告经 0.14.0 JSON Schema 校验。 +八个默认执行的端到端反例覆盖配置的 Ruff 缺失、项目未声明 Ruff、有效语法、无效 Ruff 配置、超大兄弟文件、任务稳定复扫、伪造 grammar/越界坐标/重复 JSON 键拒绝,以及持久化失败保留空任务 ID;均保持退出码 3、`delivery_decision=not_evaluated`。更改源码为有效语法后,再次 WASM 零恢复节点仍保持同一任务 `open`。额外使用本机 Ruff 0.16.8 运行 `task verify`,保存原生观察而不关闭候选任务;原生优先路径另由 `lint_python_cli` 的真实 Ruff 用例核对。实际生成的 0.15.0 反馈和 0.1.0 本地报告经 JSON Schema 校验,伪造同步成功状态被拒绝。默认二进制消费特性版保存的历史报告后,`work sync` 未出现失败报告。 -仍未完成:Python grammar 的版本/方言及语料验收、项目级多模块调度、稳定原生确认任务、能力匹配的复检关闭、公开发行和真实宿主对话渲染。因此相关 OpenSpec 总任务保持未勾选。 +仍未完成:Python grammar 的版本/方言及语料验收、多文件/项目级任务归并、能力匹配的原生复检关闭、公开发行和真实宿主对话渲染。因此相关 OpenSpec 总任务保持未勾选。