Skip to content

Commit 145bb3c

Browse files
committed
接入占位任务收据绑定诊断与旧定位撤权
1 parent 1befb0a commit 145bb3c

11 files changed

Lines changed: 568 additions & 12 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const MAX_SOURCE_BYTES: u64 = 16 * 1024 * 1024;
1818
mod c_family_comments_candidate;
1919
#[cfg(unix)]
2020
mod c_family_structure_candidate;
21+
mod c_family_placeholder_candidate;
2122

2223
struct Arguments {
2324
root: PathBuf,
@@ -448,7 +449,7 @@ fn independent_source_task_index(root: &Path, candidates: &[Candidate]) -> Optio
448449

449450
fn candidate(root: &Path, id: &str, fact: &Value) -> Result<Candidate, &'static str> {
450451
if matches!(fact["checker_id"].as_str(), Some("c.clang.documentation_placeholder" | "cpp.clang.documentation_placeholder")) {
451-
return Err("clang_placeholder_task_workflow_not_integrated");
452+
return c_family_placeholder_candidate::candidate(root, id, fact);
452453
}
453454
#[cfg(unix)]
454455
if matches!(fact["checker_id"].as_str(),Some("c.clang.documentation_structure"|"cpp.clang.documentation_structure")) {
@@ -2438,7 +2439,7 @@ fn run_sequence(run_id: &str) -> Option<u128> {
24382439

24392440
fn view(disposition: &str, reason: &str, brief: Value, actions: Value) -> Value {
24402441
json!({
2441-
"schema_version":if brief["schema_version"] == "0.33.0" {json!("0.33.0")}else if brief["schema_version"] == "0.32.0" {json!("0.32.0")}else if brief["schema_version"] == "0.31.0" {json!("0.31.0")}else if brief["schema_version"] == "0.30.0" {json!("0.30.0")}else if brief["schema_version"] == "0.29.0" {json!("0.29.0")}else if brief["schema_version"] == "0.28.0" {json!("0.28.0")}else if brief["schema_version"] == "0.27.0" {json!("0.27.0")}else if brief["schema_version"] == "0.26.0" {json!("0.26.0")}else if brief["schema_version"] == "0.25.0" {json!("0.25.0")}else if brief["schema_version"] == "0.24.0" {json!("0.24.0")}else if brief["schema_version"] == "0.23.0" {json!("0.23.0")}else if brief["schema_version"] == "0.22.0" {json!("0.22.0")}else if brief["schema_version"] == "0.21.0" {json!("0.21.0")}else if brief["schema_version"] == "0.20.0" {json!("0.20.0")}else if brief["checker_id"] == "shell.shellcheck" {json!("0.17.0")} else if brief["checker_id"] == "go.vet" {json!("0.13.0")} else if brief["checker_id"] == "syntax.native_confirmation" {brief["schema_version"].clone()} else {json!("0.1.0")}, "report_type":"repair_brief_preview",
2442+
"schema_version":if brief["schema_version"] == "0.34.0" {json!("0.34.0")}else if brief["schema_version"] == "0.33.0" {json!("0.33.0")}else if brief["schema_version"] == "0.32.0" {json!("0.32.0")}else if brief["schema_version"] == "0.31.0" {json!("0.31.0")}else if brief["schema_version"] == "0.30.0" {json!("0.30.0")}else if brief["schema_version"] == "0.29.0" {json!("0.29.0")}else if brief["schema_version"] == "0.28.0" {json!("0.28.0")}else if brief["schema_version"] == "0.27.0" {json!("0.27.0")}else if brief["schema_version"] == "0.26.0" {json!("0.26.0")}else if brief["schema_version"] == "0.25.0" {json!("0.25.0")}else if brief["schema_version"] == "0.24.0" {json!("0.24.0")}else if brief["schema_version"] == "0.23.0" {json!("0.23.0")}else if brief["schema_version"] == "0.22.0" {json!("0.22.0")}else if brief["schema_version"] == "0.21.0" {json!("0.21.0")}else if brief["schema_version"] == "0.20.0" {json!("0.20.0")}else if brief["checker_id"] == "shell.shellcheck" {json!("0.17.0")} else if brief["checker_id"] == "go.vet" {json!("0.13.0")} else if brief["checker_id"] == "syntax.native_confirmation" {brief["schema_version"].clone()} else {json!("0.1.0")}, "report_type":"repair_brief_preview",
24422443
"operation":"next", "command_status":"complete", "exit_code":0,
24432444
"disposition":disposition, "reason":reason,
24442445
"repair_brief":brief, "next_actions":actions,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//! 占位文件任务诊断指引绑定首次报告、收据与当前输入;专用修复权限仍待验证闭环。
2+
use super::Candidate;
3+
use crate::work_sync::c_family_placeholder_report::{
4+
checker, current, fingerprint, positions, valid_shape, RULE,
5+
};
6+
use serde_json::{Value, json};
7+
use sha2::{Digest, Sha256};
8+
use std::{fs, path::Path};
9+
10+
fn read(root: &Path, run: &str, digest: Option<&str>) -> Result<Value, &'static str> {
11+
if !super::safe_run_id(run) {
12+
return Err("clang_placeholder_origin_invalid");
13+
}
14+
let bytes = super::read_bounded(
15+
&root.join(format!(".codeguard/reports/{run}.json")),
16+
16 * 1024 * 1024,
17+
)?;
18+
let hash = format!("{:x}", Sha256::digest(&bytes));
19+
if digest.is_some_and(|s| s != hash) {
20+
return Err("clang_placeholder_origin_changed");
21+
}
22+
let r = codeguard_adapters::parse_unique_json(&bytes)
23+
.map_err(|_| "clang_placeholder_origin_invalid")?;
24+
if !valid_shape(&r) || r["run_id"] != run {
25+
return Err("clang_placeholder_origin_invalid");
26+
}
27+
let expected=serde_json::to_vec_pretty(&json!({"schema_version":"0.1.0","workspace_id":r["workspace_id"],"run_id":run,"report_sha256":hash})).map_err(|_|"clang_placeholder_marker_invalid")?;
28+
if super::read_bounded(
29+
&root.join(format!(".codeguard/state/consumed/{run}.json")),
30+
4096,
31+
)? != expected
32+
{
33+
return Err("clang_placeholder_observation_not_consumed");
34+
}
35+
Ok(r)
36+
}
37+
/// 创建收据绑定的诊断指引;专用验证/尝试未接入时不授予允许修改范围。
38+
pub(super) fn candidate(root: &Path, id: &str, fact: &Value) -> Result<Candidate, &'static str> {
39+
let run = fact["first_run_id"].as_str().ok_or("finding_run_invalid")?;
40+
let digest = fact["first_report_sha256"]
41+
.as_str()
42+
.ok_or("finding_report_invalid")?;
43+
let first = read(root, run, Some(digest))?;
44+
let fp = fingerprint(&first);
45+
if id != format!("CG-{}", &fp[..32])
46+
|| fact["fingerprint"] != fp
47+
|| fact["kind"] != "finding"
48+
|| fact["native_rule_id"] != RULE
49+
|| fact["checker_id"] != checker(&first)
50+
|| fact["path"] != first["path"]
51+
|| fact["workspace_id"] != first["workspace_id"]
52+
|| fact["first_source_sha256"] != first["source_sha256"]
53+
|| positions(&first).is_empty()
54+
{
55+
return Err("clang_placeholder_fact_conflict");
56+
}
57+
let score = |r: &Value| {
58+
r["run_id"]
59+
.as_str()
60+
.and_then(|s| s.rsplit('-').next())
61+
.and_then(|n| n.parse::<u128>().ok())
62+
.unwrap_or(0)
63+
};
64+
let mut latest = first.clone();
65+
let entries = fs::read_dir(root.join(".codeguard/reports"))
66+
.map_err(|_| "reports_unreadable")?
67+
.take(1001)
68+
.collect::<Result<Vec<_>, _>>()
69+
.map_err(|_| "reports_unreadable")?;
70+
if entries.len() > 1000 {
71+
return Err("clang_placeholder_history_limit");
72+
}
73+
for entry in entries {
74+
let path = entry.path();
75+
let Some(run) = path
76+
.file_stem()
77+
.and_then(|s| s.to_str())
78+
.filter(|s| s.starts_with("clangdocplaceholder-"))
79+
else {
80+
continue;
81+
};
82+
if !root
83+
.join(format!(".codeguard/state/consumed/{run}.json"))
84+
.exists()
85+
{
86+
continue;
87+
}
88+
let r = read(root, run, None)?;
89+
if r["workspace_id"] == first["workspace_id"]
90+
&& r["path"] == first["path"]
91+
&& r["language"] == first["language"]
92+
&& score(&r) > score(&latest)
93+
{
94+
latest = r;
95+
}
96+
}
97+
let tool_current = first["selected_tool"]
98+
.as_str()
99+
.and_then(|p| Path::new(p).canonicalize().ok())
100+
.and_then(|p| codeguard_runtime::read_bounded_regular_file(&p, 256 * 1024 * 1024).ok())
101+
.is_some_and(|b| first["native"]["tool_sha256"] == format!("{:x}", Sha256::digest(b)));
102+
let context = latest["selected_tool"] == first["selected_tool"]
103+
&& latest["standard"] == first["standard"]
104+
&& latest["native"]["tool_sha256"] == first["native"]["tool_sha256"];
105+
let status = if !context {
106+
"original_context_changed"
107+
} else if !tool_current || !current(root, &latest) {
108+
"input_changed"
109+
} else if positions(&latest).is_empty() {
110+
"candidate_absent_unverified_policy"
111+
} else {
112+
"placeholder_descriptions_observed"
113+
};
114+
let observed = status == "placeholder_descriptions_observed";
115+
let brief = json!({"schema_version":"0.34.0","task_id":id,"kind":"finding","checker_id":checker(&first),"scope":first["path"],"source_sha256":latest["source_sha256"],"placeholder_rule_id":RULE,"rule_source":"codeguard_structural_policy","evidence_ref":{"first_run_id":run,"first_report_sha256":digest},"current_run_id":latest["run_id"],"observation_status":status,"placeholder_positions":if observed {positions(&latest)}else{Vec::new()},"rule_basis":"整个用途、参数或适用返回说明仅为明确占位标记,不等于API契约说明;本规则不是Clang警告。","constraints":["不修改API或行为,不关闭检查器或自行批准白名单","专用尝试历史与task verify尚未接入,本视图不授予任务修复权限"],"allowed_paths":[],"affected_paths":[first["path"]],"disposition":"needs_decision","reason_code":if observed {"clang_placeholder_task_workflow_not_integrated"}else{status},"step":["依据当前组件与真实API核对占位说明;专用修复闭环未接入前保留任务。","使用绑定原工具、标准与工作区的comments命令诊断复扫;源码或工具失稳时不按历史定位修改。"],"recheck_argv":["codeguard","comments",first["language"],root.join(first["path"].as_str().ok_or("clang_placeholder_scope_invalid")?),"--clang-tool",first["selected_tool"],"--standard",first["standard"],"--workspace",root,"--format=json"],"attempt_history_status":"not_integrated","closure_condition":"原命令局部复扫不关闭任务;专用受控尝试、原工具task verify及可信关闭仍须实现并验收。","authority":"local_unverified","delivery_decision":"not_evaluated"});
116+
Ok(Candidate {
117+
id: id.into(),
118+
priority: 0,
119+
brief,
120+
})
121+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ pub fn run(args: &[String]) -> ExitCode {
9494
Ok(brief) => brief,
9595
Err(reason) => return print_unavailable(&parsed, reason),
9696
};
97+
if matches!(brief["checker_id"].as_str(), Some("c.clang.documentation_placeholder" | "cpp.clang.documentation_placeholder")) {
98+
return print_unavailable(&parsed, "clang_placeholder_task_workflow_not_integrated");
99+
}
97100
let gradle_cve_task = brief["checker_id"] == "java.gradle.dependency_check";
98101
let c_structure_task = matches!(
99102
brief["checker_id"].as_str(),

‎crates/codeguard-cli/src/work_sync/c_family_placeholder_report.rs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub(crate) fn valid_shape(report: &Value) -> bool {
3939
)
4040
}
4141

42-
fn fingerprint(report: &Value) -> String {
42+
pub(crate) fn fingerprint(report: &Value) -> String {
4343
let mut digest = Sha256::new();
4444
for part in [
4545
"clang-documentation-placeholder-policy-v1",
@@ -54,6 +54,27 @@ fn fingerprint(report: &Value) -> String {
5454
format!("{:x}", digest.finalize())
5555
}
5656

57+
/// 返回独立占位检查器身份,不能复用原警告或缺失组件检查器。
58+
pub(crate) fn checker(report: &Value) -> &'static str {
59+
if report["language"] == "c" {
60+
"c.clang.documentation_placeholder"
61+
} else {
62+
"cpp.clang.documentation_placeholder"
63+
}
64+
}
65+
/// 核验当前源码、工具及原生结构关联;不以当前状态证明可信执行。
66+
pub(crate) fn current(root: &Path, report: &Value) -> bool {
67+
valid_shape(report)
68+
&& super::c_family_structure_report::current(root, &structure_packet(report))
69+
}
70+
/// 返回独立占位组件位置,仅供已验证观察的诊断投影。
71+
pub(crate) fn positions(report: &Value) -> Vec<Value> {
72+
report["placeholders"]["positions"]
73+
.as_array()
74+
.cloned()
75+
.unwrap_or_default()
76+
}
77+
5778
/// 返回当前局部观察对应的稳定文件任务;只供持久化状态投影,不授予修复权限。
5879
pub(crate) fn task_ids(root: &Path, report: &Value) -> Vec<String> {
5980
if valid_shape(report)

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,55 @@ fn check_language(language: &str, standard: &str, extension: &str) {
9898
.as_array()
9999
.unwrap();
100100
assert_eq!(ids.len(), 1);
101+
let next = || {
102+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
103+
.arg("next")
104+
.arg(&root)
105+
.arg("--format=json")
106+
.output()
107+
.unwrap();
108+
assert!(matches!(output.status.code(), Some(0 | 3)));
109+
serde_json::from_slice::<Value>(&output.stdout).unwrap()
110+
};
111+
let guidance = next();
112+
assert_eq!(guidance["schema_version"], "0.34.0");
113+
assert_eq!(guidance["repair_brief"]["task_id"], ids[0]);
114+
assert_eq!(
115+
guidance["repair_brief"]["placeholder_positions"]
116+
.as_array()
117+
.unwrap()
118+
.len(),
119+
3
120+
);
121+
assert_eq!(
122+
guidance["repair_brief"]["allowed_paths"],
123+
serde_json::json!([])
124+
);
125+
assert_eq!(
126+
guidance["repair_brief"]["attempt_history_status"],
127+
"not_integrated"
128+
);
129+
fs::write(&source, "int changed(int x);\n").unwrap();
130+
let stale_guidance = next();
131+
assert_eq!(
132+
stale_guidance["repair_brief"]["observation_status"],
133+
"input_changed"
134+
);
135+
assert_eq!(
136+
stale_guidance["repair_brief"]["placeholder_positions"],
137+
serde_json::json!([])
138+
);
139+
fs::write(
140+
&source,
141+
"/// TODO.\n/// @param x TBD\n/// @return FIXME!\nint f(int x);\n",
142+
)
143+
.unwrap();
144+
let first_run = bound["placeholder_workbench"]["run_id"].as_str().unwrap();
145+
let marker = root.join(format!(".codeguard/state/consumed/{first_run}.json"));
146+
let marker_bytes = fs::read(&marker).unwrap();
147+
fs::write(&marker, b"{}").unwrap();
148+
assert!(next()["repair_brief"].is_null());
149+
fs::write(&marker, marker_bytes).unwrap();
101150
let repeated = scan(&tool);
102151
assert_eq!(
103152
repeated["placeholder_workbench"]["task_ids"],
@@ -129,6 +178,14 @@ fn check_language(language: &str, standard: &str, extension: &str) {
129178
blocked["placeholder_workbench"]["task_ids"],
130179
serde_json::json!([])
131180
);
181+
if let Ok(destination) = std::env::var("CODEGUARD_PLACEHOLDER_NEXT_EVIDENCE") {
182+
assert!(std::path::Path::new(&destination).is_absolute());
183+
fs::write(
184+
destination,
185+
serde_json::to_vec_pretty(&serde_json::json!([guidance, stale_guidance])).unwrap(),
186+
)
187+
.unwrap();
188+
}
132189
if let Ok(destination) = std::env::var("CODEGUARD_PLACEHOLDER_CLI_EVIDENCE") {
133190
assert!(std::path::Path::new(&destination).is_absolute());
134191
fs::write(

‎openspec/changes/introduce-rust-codeguard-cli/specs/native-tool-adapters/spec.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,3 +1320,13 @@ The standalone comments command SHALL automatically save and consume its indepen
13201320
- **THEN** it SHALL report the placeholder observation without creating .codeguard
13211321
- **WHEN** the workspace is initialized and the observation is supported
13221322
- **THEN** comments SHALL automatically consume the placeholder packet, return one stable file task and preserve it as open after a clean re-scan
1323+
1324+
### Requirement: Placeholder task diagnostics SHALL bind origins and retract stale positions
1325+
1326+
next SHALL provide a separately versioned placeholder diagnostic brief after validating immutable task identity, the first consumed report digest, workspace/path/language/policy identity and exact consumption markers. The most recent consumed same-file observation SHALL retain the original tool and standard context. Changed source/tool context or missing/contradictory receipts SHALL withdraw usable positions. The brief SHALL retain original commands, policy basis, constraints and concrete integration gaps. Until dedicated attempts and original-task verification are implemented, allowed_paths SHALL remain empty and attempt_history_status SHALL be not_integrated; no artificial history counters or repair permission SHALL be issued. This integration gap is not an approved exception or task closure.
1327+
1328+
#### Scenario: Input or consumed receipt changes
1329+
- **WHEN** the file no longer matches the latest consumed observation or an original receipt is tampered with
1330+
- **THEN** next SHALL retract historical positions or reject the brief, rather than authorize repairs at stale locations
1331+
- **WHEN** the source and origin remain current
1332+
- **THEN** diagnostic positions MAY be displayed, but task verify SHALL still reject unsupported placeholder verification before leasing, execution or event persistence

0 commit comments

Comments
 (0)