Skip to content

Commit fee2d34

Browse files
committed
为 Python 编辑反馈增加有界原生快检范围
1 parent 890c1e7 commit fee2d34

15 files changed

Lines changed: 418 additions & 24 deletions

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ Supply the original checker's required tool/configuration options to `task verif
169169
| `rules whitelist list / explain / propose` | Inspect/propose false-positive dispositions and corrections | No public approval or activation |
170170
| `gate pre-commit` | Git-index path-safety observation | Not a full content/security gate |
171171

172+
For Python edit feedback, run `codeguard lint python . --file src/changed.py --format json`. `--file` is repeatable with a limit of eight distinct paths, 512 bytes per path and 2 KiB combined. The response declares `scan_scope=selected_files` and observes only those files; it is not imported as a full workspace scan and cannot approve delivery. Without `--file`, the existing command still scans discovered Python files and synchronizes its local report.
173+
172174
Generic `dependencies`, generic `security`, arbitrary category/language combinations, `fix`, `gate pre-push`, `gate ci`, `mcp serve`, and the legacy compatibility dispatcher are target-design surfaces, not available commands in this baseline.
173175

174176
### Native-first entry points and syntax fallback — design target

‎README.zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ codeguard task verify "$TASK_ID" . --format json
168168
| `rules whitelist list / explain / propose` | 查询或提出误报处置与纠错 | 无公开批准或生效入口 |
169169
| `gate pre-commit` | Git index 路径安全观察 | 尚非完整内容或安全门禁 |
170170

171+
Python 编辑快反馈可执行 `codeguard lint python . --file src/changed.py --format json`;`--file` 可重复,最多 8 个不同路径,单路径 512 字节、总计 2 KiB。报告标明 `scan_scope=selected_files`,只观察选中文件;局部反馈不导入完整工作台,也不代表全项目或交付通过。原有不带 `--file` 的命令仍扫描发现到的 Python 文件并同步局部报告。
172+
171173
通用 `dependencies`、通用 `security`、任意类别/语言组合、`fix`、`gate pre-push`、`gate ci`、`mcp serve` 和旧协议兼容调度均属目标设计,在本基线中不能作为已实现命令调用。
172174

173175
### 原生优先的统一入口与语法兜底——设计目标

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ fn main() -> ExitCode {
2323
env!("CARGO_PKG_VERSION")
2424
);
2525
println!("check all 可汇总已接入的原生结果与未完成义务;完整质量门禁尚未实现。");
26+
println!(
27+
"lint python [path] 可重复使用 --file REL_PATH 指定最多 8 个本轮编辑文件作局部 Ruff 反馈;不会把局部报告同步成完整工作台扫描,也不签发交付通过。"
28+
);
2629
println!(
2730
"宿主事件只读规划:hook plan [--format=json] 从 stdin 读取 hook_trigger_request 1.0.0,返回 hook_trigger_plan 1.1.0 与退出码 3;批量编辑超出快检预算时要求改用有界批量范围,尚不执行检查或阻断宿主。"
2831
);

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

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use crate::check_budget::{
55
};
66
use crate::discovery::discover;
77
use crate::next_command::read_local_brief;
8-
use crate::python_lint_scan::{PythonLintScanRequest, python_lint_feedback, scan_python_lint};
8+
use crate::python_lint_scan::{
9+
PythonLintScanRequest, python_lint_feedback, scan_python_lint, select_checker,
10+
};
911
use crate::work_sync::{save_local_report, sync_local_workspace};
1012
use crate::workspace_refresh::read_workspace_baseline;
1113
use codeguard_adapters::{bundled_ruff_rulepack, legacy_registry};
@@ -14,7 +16,7 @@ use codeguard_runtime::{
1416
};
1517
use serde_json::Value;
1618
use sha2::{Digest, Sha256};
17-
use std::collections::BTreeMap;
19+
use std::collections::{BTreeMap, BTreeSet};
1820
use std::fs;
1921
use std::os::unix::fs::PermissionsExt;
2022
use std::path::{Path, PathBuf};
@@ -26,6 +28,7 @@ struct Arguments {
2628
root: PathBuf,
2729
json: bool,
2830
ruff_tool: Option<PathBuf>,
31+
selected_paths: Vec<String>,
2932
timeout_ms: u64,
3033
timeout_source: &'static str,
3134
}
@@ -74,19 +77,34 @@ pub fn run(args: &[String]) -> ExitCode {
7477
}
7578
}
7679
let deadline = started + Duration::from_millis(parsed.timeout_ms);
77-
let mut feedback = match scan_and_sync_report_with_deadline(
78-
&root,
79-
parsed.ruff_tool.as_deref(),
80-
deadline,
81-
&AtomicBool::new(false),
82-
) {
80+
let mut feedback = match if parsed.selected_paths.is_empty() {
81+
scan_and_sync_report_with_deadline(
82+
&root,
83+
parsed.ruff_tool.as_deref(),
84+
deadline,
85+
&AtomicBool::new(false),
86+
)
87+
} else {
88+
scan_selected_report_with_deadline(
89+
&root,
90+
parsed.ruff_tool.as_deref(),
91+
&parsed.selected_paths,
92+
deadline,
93+
&AtomicBool::new(false),
94+
)
95+
} {
8396
Ok(feedback) => feedback,
8497
Err(_) => {
8598
eprintln!("内置语言清单损坏");
8699
return ExitCode::from(4);
87100
}
88101
};
89102
annotate_conversation_budget(&mut feedback, parsed.timeout_ms, parsed.timeout_source);
103+
feedback["schema_version"] = Value::String("0.13.0".into());
104+
if parsed.selected_paths.is_empty() {
105+
feedback["scan_scope"] = Value::String("discovered_python".into());
106+
feedback["requested_paths"] = Value::Null;
107+
}
90108
let request_cancelled = codeguard_runtime::sigint_cancellation_requested()
91109
|| feedback["incomplete_reasons"]
92110
.as_array()
@@ -184,6 +202,41 @@ pub fn annotate_conversation_budget(feedback: &mut Value, timeout_ms: u64, sourc
184202
document.insert("execution_budget".into(), budget_record(timeout_ms, source));
185203
}
186204

205+
/// 编辑快反馈仅观察指定文件,不把局部报告作为完整工作区报告导入工作台。
206+
fn scan_selected_report_with_deadline(
207+
root: &Path,
208+
ruff_tool: Option<&Path>,
209+
selected_paths: &[String],
210+
deadline: Instant,
211+
cancelled: &AtomicBool,
212+
) -> Result<Value, &'static str> {
213+
let mut feedback = scan_local_report_scoped_with_deadline(
214+
root,
215+
ruff_tool,
216+
Some(selected_paths),
217+
deadline,
218+
cancelled,
219+
)?;
220+
let document = feedback.as_object_mut().expect("反馈根为对象");
221+
document.insert("scan_scope".into(), Value::String("selected_files".into()));
222+
document.insert("requested_paths".into(), serde_json::json!(selected_paths));
223+
document.insert(
224+
"backlog_status".into(),
225+
Value::String("not_synced_scoped".into()),
226+
);
227+
document.insert("backlog_sync".into(), Value::Null);
228+
document.insert(
229+
"repair_brief_status".into(),
230+
Value::String("unavailable".into()),
231+
);
232+
document.insert(
233+
"repair_brief_reason".into(),
234+
Value::String("not_synced_scoped".into()),
235+
);
236+
document.insert("next".into(), Value::Null);
237+
Ok(feedback)
238+
}
239+
187240
/// 对项目运行同一条局部原生 Ruff 检查链,返回可供持久同步的 0.9 报告。
188241
///
189242
/// 参数 `root` 是规范化的项目根,`ruff_tool` 是可选的绝对可执行路径;
@@ -203,13 +256,33 @@ pub fn scan_local_report_with_deadline(
203256
ruff_tool: Option<&Path>,
204257
deadline: Instant,
205258
cancelled: &AtomicBool,
259+
) -> Result<Value, &'static str> {
260+
scan_local_report_scoped_with_deadline(root, ruff_tool, None, deadline, cancelled)
261+
}
262+
263+
fn scan_local_report_scoped_with_deadline(
264+
root: &Path,
265+
ruff_tool: Option<&Path>,
266+
selected_paths: Option<&[String]>,
267+
deadline: Instant,
268+
cancelled: &AtomicBool,
206269
) -> Result<Value, &'static str> {
207270
let registry = legacy_registry().map_err(|_| "registry_invalid")?;
208271
let discovery = discover(root, &registry, &NativeObservation);
209272
let run_id = run_id();
210-
let configured = discovery.checker_configurations.iter().any(|checker| {
211-
checker.checker_id == "python.ruff" && checker.configuration == "configured"
212-
});
273+
let configured = selected_paths.map_or_else(
274+
|| {
275+
discovery.checker_configurations.iter().any(|checker| {
276+
checker.checker_id == "python.ruff" && checker.configuration == "configured"
277+
})
278+
},
279+
|paths| {
280+
paths.iter().any(|path| {
281+
select_checker(&discovery, path)
282+
.is_some_and(|checker| checker.configuration == "configured")
283+
})
284+
},
285+
);
213286
let tool = if configured {
214287
prepare_tool(root, ruff_tool, deadline, &run_id, cancelled)
215288
} else {
@@ -230,6 +303,7 @@ pub fn scan_local_report_with_deadline(
230303
&PythonLintScanRequest {
231304
root,
232305
discovery: &discovery,
306+
selected_paths,
233307
tool: tool.path,
234308
tool_unavailable_reason: tool.unavailable_reason,
235309
expected_tool_sha256: tool.sha256,
@@ -357,12 +431,13 @@ fn parse_args(args: &[String]) -> Result<Arguments, String> {
357431
let mut root = None;
358432
let mut json = false;
359433
let mut ruff_tool = None;
434+
let mut selected_paths = Vec::new();
360435
let mut timeout_ms = 0;
361436
let mut timeout_seen = false;
362437
let mut index = 1;
363438
while index < args.len() {
364439
let arg = &args[index];
365-
if arg == "--format" || arg == "--ruff-tool" || arg == "--timeout" {
440+
if arg == "--format" || arg == "--ruff-tool" || arg == "--timeout" || arg == "--file" {
366441
index += 1;
367442
let value = args.get(index).ok_or_else(|| format!("{arg} 缺少值"))?;
368443
if arg == "--format" {
@@ -373,6 +448,8 @@ fn parse_args(args: &[String]) -> Result<Arguments, String> {
373448
}
374449
timeout_ms = parse_check_timeout(value)?;
375450
timeout_seen = true;
451+
} else if arg == "--file" {
452+
selected_paths.push(value.clone());
376453
} else if ruff_tool.replace(PathBuf::from(value)).is_some() {
377454
return Err("--ruff-tool 重复".into());
378455
}
@@ -388,11 +465,34 @@ fn parse_args(args: &[String]) -> Result<Arguments, String> {
388465
if ruff_tool.as_ref().is_some_and(|tool| !tool.is_absolute()) {
389466
return Err("--ruff-tool 必须是绝对路径".into());
390467
}
468+
let mut distinct = BTreeSet::new();
469+
let mut total_bytes = 0;
470+
for path in &selected_paths {
471+
if path.is_empty()
472+
|| path.len() > 512
473+
|| path.starts_with('/')
474+
|| path.contains('\\')
475+
|| path.chars().any(char::is_control)
476+
|| path
477+
.split('/')
478+
.any(|part| part.is_empty() || part == "." || part == "..")
479+
{
480+
return Err("--file 必须是有效的工作区相对源码路径".into());
481+
}
482+
if distinct.insert(path.clone()) {
483+
total_bytes += path.len();
484+
}
485+
}
486+
if distinct.len() > 8 || total_bytes > 2048 {
487+
return Err("--file 超出局部快检范围预算,请改用批量检查".into());
488+
}
489+
let selected_paths = distinct.into_iter().collect();
391490
let (timeout_ms, timeout_source) = select_check_timeout(timeout_seen.then_some(timeout_ms))?;
392491
Ok(Arguments {
393492
root: root.unwrap_or_else(|| PathBuf::from(".")),
394493
json,
395494
ruff_tool,
495+
selected_paths,
396496
timeout_ms,
397497
timeout_source,
398498
})
@@ -532,6 +632,11 @@ fn create_scratch(run_id: &str) -> std::io::Result<PrivateScratch> {
532632

533633
fn print_human(feedback: &Value) {
534634
println!("CodeGuard Python lint:局部原生检查;完整质量门禁尚未评估");
635+
println!(
636+
"本轮范围:{};目标:{}",
637+
feedback["scan_scope"].as_str().unwrap_or("unknown"),
638+
feedback["requested_paths"]
639+
);
535640
println!(
536641
"修复记录:{}",
537642
feedback["backlog_status"].as_str().unwrap_or("unknown")

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use std::time::Instant;
1717
pub struct PythonLintScanRequest<'a> {
1818
pub root: &'a Path,
1919
pub discovery: &'a DiscoveryReport,
20+
/// 编辑快反馈的精确工作区相对路径;None 才表示本轮发现到的全部 Python 源码。
21+
pub selected_paths: Option<&'a [String]>,
2022
pub tool: Option<PathBuf>,
2123
pub tool_unavailable_reason: &'static str,
2224
pub expected_tool_sha256: [u8; 32],
@@ -218,12 +220,34 @@ pub fn scan_python_lint(
218220
output.incomplete_reasons.push("no_python_sources".into());
219221
return output;
220222
}
223+
let sources: Vec<&String> = if let Some(selected) = request.selected_paths {
224+
if selected.is_empty() {
225+
output
226+
.incomplete_reasons
227+
.push("selected_scope_empty".into());
228+
return output;
229+
}
230+
for target in selected {
231+
if !python.source_files.contains(target) {
232+
output
233+
.incomplete_reasons
234+
.push(format!("target_not_discovered:{target}"));
235+
}
236+
}
237+
python
238+
.source_files
239+
.iter()
240+
.filter(|source| selected.contains(source))
241+
.collect()
242+
} else {
243+
python.source_files.iter().collect()
244+
};
221245
let mut input_digests = BTreeMap::new();
222-
for source in &python.source_files {
246+
for source in &sources {
223247
let path = request.root.join(source);
224248
match digest_file(&path, 16 * 1024 * 1024) {
225249
Some(digest) => {
226-
input_digests.insert(source.clone(), digest);
250+
input_digests.insert((*source).clone(), digest);
227251
}
228252
None => {
229253
output
@@ -238,7 +262,13 @@ pub fn scan_python_lint(
238262
.checker_configurations
239263
.iter()
240264
.filter(|checker| {
241-
checker.checker_id == "python.ruff" && checker.configuration == "configured"
265+
checker.checker_id == "python.ruff"
266+
&& checker.configuration == "configured"
267+
&& sources.iter().any(|source| {
268+
select_checker(request.discovery, source).is_some_and(|selected| {
269+
selected.configuration_ref == checker.configuration_ref
270+
})
271+
})
242272
})
243273
{
244274
if let Some(digest) =
@@ -251,7 +281,8 @@ pub fn scan_python_lint(
251281
.push(format!("config_unavailable:{}", checker.configuration_ref));
252282
}
253283
}
254-
for (index, source) in python.source_files.iter().enumerate() {
284+
for (index, source) in sources.iter().enumerate() {
285+
let source = *source;
255286
let Some(checker) = select_checker(request.discovery, source) else {
256287
output
257288
.files
@@ -335,7 +366,7 @@ pub fn scan_python_lint(
335366
reason = Some("finding_identity_unavailable".into());
336367
}
337368
output.files.push(PythonLintFileResult {
338-
path: source.clone(),
369+
path: (*source).clone(),
339370
config_ref: Some(checker.configuration_ref.clone()),
340371
config_sha256: completion.then(|| hex_digest(&config_digest)),
341372
tool_sha256: completion.then(|| hex_digest(&request.expected_tool_sha256)),

0 commit comments

Comments
 (0)