Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。

Expand Down
2 changes: 2 additions & 0 deletions crates/codeguard-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion crates/codeguard-cli/src/next_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,15 @@ fn candidate(root: &Path, id: &str, fact: &Value) -> Result<Candidate, &'static
brief["build_root"] = json!(build_root);
brief["affected_paths"] = json!(paths);
brief["constraints"] = json!(["先恢复检查完整性", "不得关闭检查器或修改无关源码"]);
let (priority, disposition, step) = if checker_id == "python.ruff.doctor" {
let (priority, disposition, step) = if checker_id == "python.ruff"
&& reason == "python_syntax_confirmation_needed"
{
(
1,
"actionable",
"核对候选 Python 疑似位置,使用当前源码和适用原生语法能力复检;未完成前不按候选观察修改源码或关闭任务",
)
Comment on lines +533 to +540

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point syntax tasks at the task verification command

This special case changes the guidance text but leaves the previously computed generic recheck_argv as codeguard lint python .. Running that command only produces another candidate scan and does not record the native verification event for this task; the generated task Markdown correctly requires codeguard task verify <id> . --ruff-tool .... Set the repair brief's argv accordingly so users following codeguard next can actually perform the requested confirmation.

Useful? React with 👍 / 👎.

} else if checker_id == "python.ruff.doctor" {
brief["first_diagnostic_reason"] = fact["first_diagnostic_reason"].clone();
(
1,
Expand Down
20 changes: 20 additions & 0 deletions crates/codeguard-cli/src/python_lint_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ pub fn run(args: &[String]) -> 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;
}
}
Expand Down Expand Up @@ -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");
}
Expand Down
264 changes: 264 additions & 0 deletions crates/codeguard-cli/src/python_syntax_confirmation.rs
Original file line number Diff line number Diff line change
@@ -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<String, &'static str> {
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()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize Python syntax run IDs when ordering task events

After task verify records a native observation, a later candidate rescan writes an observed event using this python-syntax-<pid>-<nanos> run ID. Both event-ordering implementations in work_sync.rs and next_command.rs only parse the existing run-ID prefixes, so the latter skips this newer event and continues presenting the older native verification as current—even when the source changed or Ruff became unavailable again. Use a recognized run-ID format or extend both sequence parsers so subsequent candidate observations invalidate stale verification guidance.

Useful? React with 👍 / 👎.

"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)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the deadline while synchronizing the confirmation report

For a bound lint python --file ... --timeout ... fallback, this call occurs after the last deadline check and invokes sync_local_workspace, which scans every queued JSON report—up to 1,000 reports of 16 MiB each—without receiving or checking the command deadline or cancellation state. A large report queue can therefore make the editor-oriented command run well past its advertised timeout; the synchronization path needs to remain deadline-aware or be limited to the newly written report.

Useful? React with 👍 / 👎.

if summary.failed_reports != 0 {
return Err("syntax_confirmation_sync_incomplete");
Comment on lines +110 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not hide a task imported beside an unrelated bad report

If any unrelated malformed or stale report is already present in .codeguard/reports, sync_local_workspace can successfully import this new confirmation report and create its task while also returning a nonzero aggregate failed_reports. This branch then reports task_persistence=unavailable with a null task ID even though the task exists, and the queued bad report causes the same false failure on every rescan. Success should be determined from the receipt/task for this report rather than failures elsewhere in the queue.

Useful? React with 👍 / 👎.

}
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::<u32>().is_ok_and(|value| value > 0)
&& nanos.parse::<u128>().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
}
Loading
Loading