Skip to content

Commit af20dd8

Browse files
committed
feat: 在全项目候选初检中优先采用已完成的 Ruff 结果
1 parent 02457ed commit af20dd8

20 files changed

Lines changed: 5797 additions & 35 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ The current source-built CLI's `codeguard grammar status --format=json` reports
102102

103103
In a source build with `--features wasm-precheck`, `codeguard grammar probe <language> <file> --format=json` can explicitly run any of the 32 pinned candidates in an isolated worker. It always exits 3 with `status=incomplete`, `native.status=not_run`, and `delivery_decision=not_evaluated`; a completed parse also has `precheck.status=incomplete`. Input or worker failures use the same [closed JSON schema](schemas/grammar-probe-v0.1.schema.json). Recovery anchors are suspected observations only. This diagnostic command does not yet connect all grammars to native-first `lint/check` or to the published npm package.
104104

105-
The same source build now runs a bounded candidate pass from `codeguard check all . --format=json` after existing native checks. Its `syntax_candidates` field in [check feedback 0.32.0](schemas/check-feedback.schema.json) distinguishes TSX, JavaScript, CFScript and explicitly embedded CFQuery, preserving native blockers and skipped work. Four bounded integration fixtures collectively invoked all 32 pinned grammars through this command. Every observation remains unqualified and the command exits 3; 64 files/64 fragments/90 seconds bound the candidate pass. This is not yet a language-qualified native fallback or part of published npm `0.1.2`. See [acceptance evidence](tests/acceptance/check-all-32-grammar-candidates.md).
105+
The same source build now runs a bounded candidate pass from `codeguard check all . --format=json` after existing native checks. Its `syntax_candidates` field in [check feedback 0.33.0](schemas/check-feedback.schema.json) distinguishes TSX, JavaScript, CFScript and explicitly embedded CFQuery, preserving native blockers and skipped work. Python files covered by a completed Ruff scan of identical source bytes skip duplicate WASM and count under `native_preferred_count`; files without matching native syntax evidence retain candidate prechecks. Four bounded integration fixtures collectively invoked all 32 pinned grammars through this command. Every observation remains unqualified and the command exits 3; 64 files/64 fragments/90 seconds bound the candidate pass. This is not yet a language-qualified native fallback or part of published npm `0.1.2`. See [acceptance evidence](tests/acceptance/check-all-32-grammar-candidates.md).
106106

107107
The source-built Unix CLI also has a narrow Zig route: `codeguard lint zig FILE --zig-tool /absolute/path/to/zig --format=json`. An explicitly supplied tool reporting Zig 0.16.0 and retaining the same byte digest runs native `ast-check` first; its source positions are retained without exposing source snippets. Without an explicit tool, the pinned Zig WASM provides an unqualified observation. Both paths remain incomplete because `ast-check` covers only local AST errors, not full lint, build or tests. See the [Zig feedback schema](schemas/zig-lint-feedback-v0.1.schema.json).
108108

‎README.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ cargo build --locked -p codeguard-cli
102102

103103
源码以 `--features wasm-precheck` 构建后,可显式运行 `codeguard grammar probe <language> <file> --format=json`,由隔离 worker 调用 32 份固定候选中的任意一份。命令始终以退出码 3 返回,并标记 `status=incomplete`、`native.status=not_run`、`delivery_decision=not_evaluated`;解析完成时还标记 `precheck.status=incomplete`。输入或 worker 失败也遵守同一份[封闭 JSON Schema](schemas/grammar-probe-v0.1.schema.json)。恢复锚点只是疑似观察。此诊断入口尚未把全部语种接入原生优先的 `lint/check`,也尚未进入已发布 npm 包。
104104

105-
同一源码构建现在会在既有原生检查之后,从 `codeguard check all . --format=json` 执行有界候选初检。[检查反馈 0.32.0](schemas/check-feedback.schema.json)中的 `syntax_candidates` 区分 TSX、JavaScript、CFScript 和明确嵌入的 CFQuery,同时保留原生阻塞与未执行范围。四个有界集成样例经此入口合计调用了 32 种固定 grammar。观察仍未验收,命令保持退出码 3;候选阶段限 64 文件、64 片段、90 秒。这尚非逐语言验收的原生兜底,也未进入已发布 npm `0.1.2`;见[验收记录](tests/acceptance/check-all-32-grammar-candidates.md)。
105+
同一源码构建现在会在既有原生检查之后,从 `codeguard check all . --format=json` 执行有界候选初检。[检查反馈 0.33.0](schemas/check-feedback.schema.json)中的 `syntax_candidates` 区分 TSX、JavaScript、CFScript 和明确嵌入的 CFQuery,同时保留原生阻塞与未执行范围。Ruff 已对相同源码完成原生扫描的 Python 文件跳过重复 WASM,并计入 `native_preferred_count`;其余未确认语法能力的文件仍运行候选初检。四个有界集成样例经此入口合计调用了 32 种固定 grammar。观察仍未验收,命令保持退出码 3;候选阶段限 64 文件、64 片段、90 秒。这尚非逐语言验收的原生兜底,也未进入已发布 npm `0.1.2`;见[验收记录](tests/acceptance/check-all-32-grammar-candidates.md)。
106106

107107
源码构建的 Unix CLI 还提供局部 Zig 入口:`codeguard lint zig FILE --zig-tool /absolute/path/to/zig --format=json`。显式提供且报告 Zig 0.16.0、执行前后字节摘要一致的工具优先运行原生 `ast-check`,仅输出诊断位置,不回显源码;未显式提供工具时,固定 Zig WASM 给出未验收候选观察。两条路径都保持未完成,因为 `ast-check` 仅覆盖局部 AST 错误,不等于完整 lint、构建或测试。见 [Zig 报告 Schema](schemas/zig-lint-feedback-v0.1.schema.json)。
108108

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,7 @@ pub fn run(args: &[String]) -> ExitCode {
14551455
let syntax_candidates = crate::check_syntax_candidates::observe(
14561456
&root,
14571457
&discovery,
1458+
&python_lint,
14581459
parsed.selection == Selection::Java,
14591460
deadline,
14601461
if request_cancelled {
@@ -1470,11 +1471,11 @@ pub fn run(args: &[String]) -> ExitCode {
14701471
"status":"not_run","reason":"binary_without_wasm_precheck","execution_phase":"after_native",
14711472
"authority":"candidate_unqualified","delivery_decision":"incomplete",
14721473
"source_file_count":discovery.languages.values().map(|item| item.source_files.len()).sum::<usize>(),
1473-
"skipped_count":0,"unrouted_count":0,"observations":[],
1474+
"skipped_count":0,"unrouted_count":0,"native_preferred_count":0,"observations":[],
14741475
"next_action":"使用包含固定语法资产的发行包运行候选初检,并完成适用原生检查"
14751476
});
14761477
let report = json!({
1477-
"schema_version":"0.32.0", "report_type":"check_feedback",
1478+
"schema_version":"0.33.0", "report_type":"check_feedback",
14781479
"operation":"check", "selection":parsed.selection.as_str(), "command_status":if request_cancelled { "cancelled" } else { "incomplete" },
14791480
"exit_code":if request_cancelled { 130 } else { 3 }, "delivery_decision":if parsed.selection == Selection::All { "incomplete" } else { "not_evaluated" }, "authority":"local_unverified",
14801481
"reason":if request_cancelled { "request_cancelled" } else if parsed.selection == Selection::All { "full_project_obligations_and_trusted_policy_unavailable" } else { "java_selection_obligations_and_trusted_policy_unavailable" },
@@ -1854,10 +1855,17 @@ pub fn run(args: &[String]) -> ExitCode {
18541855
}
18551856
if let Some(observations) = report["syntax_candidates"]["observations"].as_array() {
18561857
if report["syntax_candidates"]["status"] == "not_run" {
1857-
println!(
1858-
"候选语法初检未运行:{};仍需适用原生检查。",
1859-
report["syntax_candidates"]["reason"]
1860-
);
1858+
if report["syntax_candidates"]["reason"] == "native_preferred" {
1859+
println!(
1860+
"候选语法初检未运行:{} 个文件已由本轮原生工具检查;其余交付义务仍待核验。",
1861+
report["syntax_candidates"]["native_preferred_count"]
1862+
);
1863+
} else {
1864+
println!(
1865+
"候选语法初检未运行:{};仍需适用原生检查。",
1866+
report["syntax_candidates"]["reason"]
1867+
);
1868+
}
18611869
} else {
18621870
let suspected = observations
18631871
.iter()
@@ -1868,12 +1876,13 @@ pub fn run(args: &[String]) -> ExitCode {
18681876
})
18691877
.count();
18701878
println!(
1871-
"候选语法初检:{} 个片段已观察,{} 个存在待原生确认的恢复节点,{} 个范围未执行;仍未完成完整检查。",
1879+
"候选语法初检:{} 个片段已观察,{} 个存在待原生确认的恢复节点,{} 个文件由原生检查覆盖,{} 个范围未执行;仍未完成完整检查。",
18721880
observations
18731881
.iter()
18741882
.filter(|item| item["status"] == "candidate_observed")
18751883
.count(),
18761884
suspected,
1885+
report["syntax_candidates"]["native_preferred_count"],
18771886
report["syntax_candidates"]["skipped_count"]
18781887
);
18791888
for item in observations

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::grammar_probe_command::read_plain_source;
55
use crate::grammar_route::route_source;
66
use crate::syntax_worker_runner::run_syntax_worker_candidate;
77
use serde_json::{Value, json};
8+
use sha2::{Digest, Sha256};
89
use std::collections::BTreeSet;
910
use std::path::Path;
1011
use std::sync::atomic::AtomicBool;
@@ -19,6 +20,7 @@ const MAX_VISIBLE_RECOVERIES: usize = 8;
1920
pub fn observe(
2021
root: &Path,
2122
discovery: &DiscoveryReport,
23+
python_lint: &Value,
2224
java_only: bool,
2325
deadline: Instant,
2426
skip_reason: Option<&str>,
@@ -33,13 +35,15 @@ pub fn observe(
3335
let mut observations = Vec::new();
3436
let mut skipped_count = 0;
3537
let mut unrouted_count = 0;
38+
let mut native_preferred_count = 0;
3639
if let Some(reason) = skip_reason {
3740
return report(
3841
"not_run",
3942
reason,
4043
source_file_count,
4144
source_file_count,
4245
0,
46+
0,
4347
observations,
4448
);
4549
}
@@ -52,6 +56,7 @@ pub fn observe(
5256
source_file_count,
5357
source_file_count,
5458
0,
59+
0,
5560
observations,
5661
);
5762
}
@@ -78,6 +83,10 @@ pub fn observe(
7883
continue;
7984
}
8085
};
86+
if native_python_covers(python_lint, relative, &source) {
87+
native_preferred_count += 1;
88+
continue;
89+
}
8190
let routes = route_source(relative, &source);
8291
if routes.is_empty() {
8392
unrouted_count += 1;
@@ -146,6 +155,8 @@ pub fn observe(
146155
"unrouted_source"
147156
} else if !observed && !observations.is_empty() {
148157
"candidate_unavailable"
158+
} else if native_preferred_count > 0 && observations.is_empty() {
159+
"native_preferred"
149160
} else {
150161
"candidate_unqualified"
151162
};
@@ -155,23 +166,71 @@ pub fn observe(
155166
source_file_count,
156167
skipped_count,
157168
unrouted_count,
169+
native_preferred_count,
158170
observations,
159171
)
160172
}
161173

174+
// 仅本轮 Ruff 已完整扫描同一份 Python 字节时跳过重复解析;配置发现不算执行。
175+
fn native_python_covers(python_lint: &Value, relative: &str, source: &[u8]) -> bool {
176+
if !relative.ends_with(".py") {
177+
return false;
178+
}
179+
let digest = format!("{:x}", Sha256::digest(source));
180+
python_lint["files"].as_array().is_some_and(|files| {
181+
files.iter().any(|file| {
182+
file["path"] == relative
183+
&& matches!(
184+
file["run_status"].as_str(),
185+
Some("passed" | "findings" | "suppressed")
186+
)
187+
&& file["source_sha256"] == digest
188+
&& file["tool_sha256"].as_str().is_some()
189+
&& file["config_sha256"].as_str().is_some()
190+
})
191+
})
192+
}
193+
162194
fn report(
163195
status: &str,
164196
reason: &str,
165197
source_file_count: usize,
166198
skipped_count: usize,
167199
unrouted_count: usize,
200+
native_preferred_count: usize,
168201
observations: Vec<Value>,
169202
) -> Value {
170203
json!({
171204
"status":status,"reason":reason,"execution_phase":"after_native",
172205
"authority":"candidate_unqualified","delivery_decision":"incomplete",
173206
"source_file_count":source_file_count,"skipped_count":skipped_count,
174-
"unrouted_count":unrouted_count,"observations":observations,
207+
"unrouted_count":unrouted_count,"native_preferred_count":native_preferred_count,"observations":observations,
175208
"next_action":"核对适用语言版本与原生 lint/编译器;候选恢复节点不是已确认违规,零恢复也不表示完整通过"
176209
})
177210
}
211+
212+
#[cfg(test)]
213+
mod tests {
214+
use super::native_python_covers;
215+
use serde_json::json;
216+
use sha2::{Digest, Sha256};
217+
218+
#[test]
219+
fn only_completed_matching_native_python_scan_preempts_candidate() {
220+
let source = b"import os\n";
221+
let digest = format!("{:x}", Sha256::digest(source));
222+
for status in ["passed", "findings", "suppressed"] {
223+
let native = json!({"files":[{"path":"app.py","run_status":status,
224+
"source_sha256":digest,"tool_sha256":"tool","config_sha256":"config"}]});
225+
assert!(native_python_covers(&native, "app.py", source));
226+
assert!(!native_python_covers(&native, "other.py", source));
227+
assert!(!native_python_covers(&native, "app.rs", source));
228+
assert!(!native_python_covers(&native, "app.py", b"changed\n"));
229+
}
230+
for status in ["incomplete", "not_run"] {
231+
let native = json!({"files":[{"path":"app.py","run_status":status,
232+
"source_sha256":digest,"tool_sha256":"tool","config_sha256":"config"}]});
233+
assert!(!native_python_covers(&native, "app.py", source));
234+
}
235+
}
236+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ fn main() -> ExitCode {
7272
);
7373
#[cfg(all(feature = "wasm-precheck", unix))]
7474
println!(
75-
"check all 会在原生节点之后按源码方言有界执行 32 份固定 WASM 的候选初检;check_feedback 0.32.0 的 syntax_candidates 保留疑似位置和未执行范围,始终不能代替原生义务或放行交付。"
75+
"check all 会在原生节点之后按源码方言有界执行 32 份固定 WASM 的候选初检;check_feedback 0.33.0 的 syntax_candidates 保留疑似位置、原生优先计数和未执行范围,始终不能代替原生义务或放行交付。"
7676
);
7777
#[cfg(all(feature = "wasm-precheck", unix))]
7878
println!(

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,61 @@ use std::collections::BTreeSet;
44
use std::fs;
55
use std::process::Command;
66

7+
#[test]
8+
fn completed_native_ruff_preempts_only_its_matching_python_file() {
9+
use std::os::unix::fs::PermissionsExt;
10+
let root = std::env::temp_dir()
11+
.canonicalize()
12+
.unwrap()
13+
.join(format!("codeguard-native-preferred-{}", std::process::id()));
14+
fs::create_dir_all(&root).unwrap();
15+
fs::write(root.join("app.py"), "import os\n").unwrap();
16+
fs::write(root.join("main.zig"), "const Empty = struct {};\n").unwrap();
17+
fs::write(root.join("ruff.toml"), "[lint]\nselect = ['F401']\n").unwrap();
18+
let tool = root.join("fake-ruff");
19+
fs::write(&tool, "#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo 'ruff 0.16.8'; exit 0; fi\nif [ \"$2\" = '--show-files' ]; then echo \"$3\"; exit 0; fi\nif [ \"$2\" = '--show-settings' ]; then printf 'linter.rules.enabled = [\\n\\tunused-import (F401),\\n]\\nlinter.per_file_ignores = {}\\n'; exit 0; fi\nif [ \"$2\" = '--no-cache' ]; then if [ \"$3\" = '--ignore-noqa' ]; then source=$6; else source=$5; fi; printf '[{\"code\":\"F401\",\"message\":\"unused\",\"filename\":\"%s\",\"location\":{\"row\":1,\"column\":1},\"severity\":\"error\"}]\\n' \"$source\"; exit 1; fi\nexit 2\n").unwrap();
20+
fs::set_permissions(&tool, fs::Permissions::from_mode(0o700)).unwrap();
21+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
22+
.args(["check", "all"])
23+
.arg(&root)
24+
.args([
25+
"--ruff-tool",
26+
tool.to_str().unwrap(),
27+
"--format=json",
28+
"--timeout",
29+
"40s",
30+
])
31+
.output()
32+
.unwrap();
33+
fs::remove_dir_all(&root).unwrap();
34+
assert_eq!(
35+
output.status.code(),
36+
Some(3),
37+
"{}",
38+
String::from_utf8_lossy(&output.stderr)
39+
);
40+
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
41+
assert_eq!(
42+
report["native_results"]["python_lint"]["files"][0]["run_status"], "findings",
43+
"{report}"
44+
);
45+
assert_eq!(
46+
report["native_results"]["python_lint"]["files"][0]["findings"][0]["rule_id"],
47+
"F401"
48+
);
49+
assert_eq!(report["syntax_candidates"]["native_preferred_count"], 1);
50+
let observations = report["syntax_candidates"]["observations"]
51+
.as_array()
52+
.unwrap();
53+
assert!(!observations.iter().any(|row| row["path"] == "app.py"));
54+
assert!(
55+
observations
56+
.iter()
57+
.any(|row| row["path"] == "main.zig" && row["status"] == "candidate_observed")
58+
);
59+
assert_eq!(report["delivery_decision"], "incomplete");
60+
}
61+
762
#[test]
863
fn check_all_routes_distinct_dialects_after_native_without_claiming_clean() {
964
let root = std::env::temp_dir()
@@ -37,7 +92,7 @@ fn check_all_routes_distinct_dialects_after_native_without_claiming_clean() {
3792
String::from_utf8_lossy(&output.stderr)
3893
);
3994
let report: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
40-
assert_eq!(report["schema_version"], "0.32.0");
95+
assert_eq!(report["schema_version"], "0.33.0");
4196
assert_eq!(report["delivery_decision"], "incomplete");
4297
let observations = report["syntax_candidates"]["observations"]
4398
.as_array()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ fn missing_p3c_configuration_is_reported_without_running_maven() {
219219
fs::set_permissions(&tool, fs::Permissions::from_mode(0o700)).unwrap();
220220
let report = project.check(&["--maven-tool", tool.to_str().unwrap()]);
221221
assert!(!marker.exists());
222-
assert_eq!(report["schema_version"], "0.32.0");
222+
assert_eq!(report["schema_version"], "0.33.0");
223223
assert_eq!(report["execution_tasks"][0]["id"], "java.p3c");
224224
assert_eq!(report["execution_tasks"][0]["status"], "native_incomplete");
225225
let java = &report["native_results"]["java_p3c"];

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ fn real_npm_audit_enters_check_all_workbench() {
411411
String::from_utf8_lossy(&out.stderr)
412412
);
413413
let report: Value = serde_json::from_slice(&out.stdout).unwrap();
414-
assert_eq!(report["schema_version"], "0.32.0");
414+
assert_eq!(report["schema_version"], "0.33.0");
415415
assert_eq!(
416416
report["native_results"]["npm_cve"][0]["feedback"]["local_coherent"], true,
417417
"{report}"

0 commit comments

Comments
 (0)