Skip to content

Commit 91e882d

Browse files
committed
接入独立占位文档包的稳定任务导入与消费验收
1 parent 1fa41c5 commit 91e882d

10 files changed

Lines changed: 531 additions & 4 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,9 @@ fn independent_source_task_index(root: &Path, candidates: &[Candidate]) -> Optio
447447
}
448448

449449
fn candidate(root: &Path, id: &str, fact: &Value) -> Result<Candidate, &'static str> {
450+
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+
}
450453
#[cfg(unix)]
451454
if matches!(fact["checker_id"].as_str(),Some("c.clang.documentation_structure"|"cpp.clang.documentation_structure")) {
452455
return c_family_structure_candidate::candidate(root,id,fact);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod checkstyle_report;
2424
pub(crate) mod c_family_comments_report;
2525
#[cfg(unix)]
2626
pub(crate) mod c_family_structure_report;
27+
pub(crate) mod c_family_placeholder_report;
2728
#[cfg(unix)]
2829
mod javadoc_report;
2930
#[cfg(unix)]
@@ -630,6 +631,9 @@ fn parse_report(
630631
digest,
631632
);
632633
}
634+
if report["report_type"] == "clang_documentation_placeholder_workbench_observation" {
635+
return c_family_placeholder_report::parse(root, workspace_id, path, report, digest);
636+
}
633637
if report["report_type"] == "clang_documentation_structure_workbench_observation" {
634638
return c_family_structure_report::parse(root, workspace_id, path, report, digest);
635639
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//! 明确占位文档的独立文件策略任务导入;不关闭问题或借用结构复检权限。
2+
use super::{FindingInput, ReportInput};
3+
use serde_json::Value;
4+
use sha2::{Digest, Sha256};
5+
use std::path::{Path, PathBuf};
6+
7+
/// 占位描述自有规则,与原生警告及缺失组件策略保持不同身份。
8+
pub(crate) const RULE: &str = "codeguard.documentation.placeholder_description";
9+
10+
fn structure_packet(report: &Value) -> Value {
11+
let mut packet = report.clone();
12+
if let Some(object) = packet.as_object_mut() {
13+
object.remove("placeholders");
14+
}
15+
packet["report_type"] = Value::from("clang_documentation_structure_workbench_observation");
16+
packet["profile"] = Value::from("clang-documentation-structure-v1");
17+
packet["run_id"] = Value::from(report["run_id"].as_str().unwrap_or("").replacen(
18+
"clangdocplaceholder-",
19+
"clangdocstruct-",
20+
1,
21+
));
22+
packet
23+
}
24+
25+
/// 校验独立封闭占位包及原生结构关联;不以本地报告证明可信执行。
26+
pub(crate) fn valid_shape(report: &Value) -> bool {
27+
report.is_object()
28+
&& report["report_type"] == "clang_documentation_placeholder_workbench_observation"
29+
&& report["profile"] == "clang-documentation-placeholder-v1"
30+
&& report["run_id"]
31+
.as_str()
32+
.is_some_and(|run| run.starts_with("clangdocplaceholder-"))
33+
&& super::c_family_structure_report::valid_shape(&structure_packet(report))
34+
&& report["structure"]["status"] == "observed"
35+
&& codeguard_adapters::valid_clang_documentation_placeholders(
36+
&report["placeholders"],
37+
&report["structure"]["observation"],
38+
None,
39+
)
40+
}
41+
42+
fn fingerprint(report: &Value) -> String {
43+
let mut digest = Sha256::new();
44+
for part in [
45+
"clang-documentation-placeholder-policy-v1",
46+
report["path"].as_str().unwrap_or(""),
47+
report["language"].as_str().unwrap_or(""),
48+
report["standard"].as_str().unwrap_or(""),
49+
RULE,
50+
] {
51+
digest.update((part.len() as u64).to_be_bytes());
52+
digest.update(part.as_bytes());
53+
}
54+
format!("{:x}", digest.finalize())
55+
}
56+
57+
/// 导入当前源码关联的占位文件任务;位置漂移复用身份,消失仍不关闭历史任务。
58+
pub(super) fn parse(
59+
root: &Path,
60+
workspace: &str,
61+
file: &Path,
62+
report: &Value,
63+
digest: String,
64+
) -> Result<ReportInput, &'static str> {
65+
if !valid_shape(report) || report["workspace_id"] != workspace {
66+
return Err("clang_placeholder_report_invalid");
67+
}
68+
let run = report["run_id"].as_str().ok_or("report_run_id_invalid")?;
69+
if file.file_stem().and_then(|stem| stem.to_str()) != Some(run) {
70+
return Err("report_run_id_invalid");
71+
}
72+
let mut findings = Vec::new();
73+
if super::c_family_structure_report::current(root, &structure_packet(report)) {
74+
let path = report["path"]
75+
.as_str()
76+
.ok_or("clang_placeholder_report_invalid")?;
77+
let snapshot = codeguard_runtime::SourceSnapshot::capture(
78+
root,
79+
[PathBuf::from(path)],
80+
1,
81+
1024 * 1024,
82+
1024 * 1024,
83+
)
84+
.map_err(|_| "clang_placeholder_source_unavailable")?;
85+
let source = snapshot
86+
.files()
87+
.get(&PathBuf::from(path))
88+
.ok_or("clang_placeholder_source_unavailable")?;
89+
if !codeguard_adapters::valid_clang_documentation_placeholders(
90+
&report["placeholders"],
91+
&report["structure"]["observation"],
92+
Some(source),
93+
) {
94+
return Err("clang_placeholder_source_mismatch");
95+
}
96+
let positions = report["placeholders"]["positions"]
97+
.as_array()
98+
.ok_or("clang_placeholder_report_invalid")?;
99+
if !positions.is_empty() {
100+
let fp = fingerprint(report);
101+
findings.push(FindingInput {
102+
checker_id: if report["language"] == "c" {
103+
"c.clang.documentation_placeholder"
104+
} else {
105+
"cpp.clang.documentation_placeholder"
106+
}
107+
.into(),
108+
id: format!("CG-{}", &fp[..32]),
109+
fingerprint: fp,
110+
path: path.into(),
111+
source_sha256: report["source_sha256"].as_str().unwrap_or("").into(),
112+
rule_id: RULE.into(),
113+
line: positions
114+
.iter()
115+
.filter_map(|p| p["line"].as_u64())
116+
.min()
117+
.unwrap_or(1),
118+
});
119+
}
120+
}
121+
Ok(ReportInput {
122+
workspace_id: workspace.into(),
123+
run_id: run.into(),
124+
digest,
125+
findings,
126+
blockers: Vec::new(),
127+
historical_findings: 0,
128+
})
129+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use serde_json::{Value, json};
2+
use std::{fs, process::Command, path::Path};
3+
4+
#[test]
5+
#[ignore = "requires explicit existing Clang via CODEGUARD_CLANG_BIN"]
6+
fn placeholder_import_reuses_file_identity_and_never_closes_on_clean_observation() {
7+
let tool = std::env::var("CODEGUARD_CLANG_BIN").unwrap();
8+
let root = std::env::temp_dir().join(format!(
9+
"cg-placeholder-import-{}-{}",
10+
std::process::id(),
11+
std::time::SystemTime::now()
12+
.duration_since(std::time::UNIX_EPOCH)
13+
.unwrap()
14+
.as_nanos()
15+
));
16+
fs::create_dir(&root).unwrap();
17+
let root = root.canonicalize().unwrap();
18+
struct Cleanup(std::path::PathBuf);
19+
impl Drop for Cleanup {
20+
fn drop(&mut self) {
21+
let _ = fs::remove_dir_all(&self.0);
22+
}
23+
}
24+
let _cleanup = Cleanup(root.clone());
25+
let source = root.join("api.c");
26+
let cli = |args: Vec<String>| {
27+
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
28+
.args(args)
29+
.arg("--format=json")
30+
.output()
31+
.unwrap();
32+
assert!(
33+
matches!(output.status.code(), Some(0 | 3)),
34+
"{}",
35+
String::from_utf8_lossy(&output.stderr)
36+
);
37+
serde_json::from_slice::<Value>(&output.stdout).unwrap()
38+
};
39+
cli(vec![
40+
"init".into(),
41+
root.display().to_string(),
42+
"--apply".into(),
43+
]);
44+
let scan = || {
45+
cli(vec![
46+
"comments".into(),
47+
"c".into(),
48+
source.display().to_string(),
49+
"--clang-tool".into(),
50+
tool.clone(),
51+
"--standard".into(),
52+
"c11".into(),
53+
])
54+
};
55+
let import = |feedback: &Value, index: usize| {
56+
let run = format!("clangdocplaceholder-1-1-{index}");
57+
let packet = json!({"schema_version":"0.1.0","report_type":"clang_documentation_placeholder_workbench_observation","workspace_binding":"bound","workspace_id":feedback["workbench"]["workspace_id"],"run_id":run,"path":"api.c","language":"c","standard":"c11","profile":"clang-documentation-placeholder-v1","selected_tool":tool,"source_sha256":feedback["source_sha256"],"local_scan_complete":feedback["local_scan_complete"],"native":feedback["native"],"structure":feedback["documentation_structure"],"placeholders":feedback["documentation_placeholders"]["observation"],"authority":"local_unverified","coverage_proven":false,"delivery_decision":"not_evaluated"});
58+
fs::write(
59+
root.join(format!(".codeguard/reports/{run}.json")),
60+
serde_json::to_vec_pretty(&packet).unwrap(),
61+
)
62+
.unwrap();
63+
cli(vec![
64+
"work".into(),
65+
"sync".into(),
66+
root.display().to_string(),
67+
]);
68+
assert!(
69+
root.join(format!(".codeguard/state/consumed/{run}.json"))
70+
.is_file()
71+
);
72+
};
73+
let facts = || {
74+
fs::read_dir(root.join(".codeguard/findings"))
75+
.unwrap()
76+
.map(|entry| entry.unwrap().path())
77+
.filter(|path| path.is_dir())
78+
.collect::<Vec<_>>()
79+
};
80+
let initial = "/// TODO.\n/// @param x TBD\n/// @return FIXME!\nint f(int x);\n";
81+
fs::write(&source, initial).unwrap();
82+
import(&scan(), 1);
83+
let first = facts();
84+
assert_eq!(first.len(), 1);
85+
let fact_path = first[0].join("finding.json");
86+
let fact: Value = serde_json::from_slice(&fs::read(&fact_path).unwrap()).unwrap();
87+
assert_eq!(fact["checker_id"], "c.clang.documentation_placeholder");
88+
assert_eq!(
89+
fact["native_rule_id"],
90+
"codeguard.documentation.placeholder_description"
91+
);
92+
fs::write(&source, format!("\n{initial}")).unwrap();
93+
import(&scan(), 2);
94+
assert_eq!(facts(), first);
95+
fs::write(&source,"/// Return the input unchanged.\n/// @param x Input value.\n/// @return The same value.\nint f(int x);\n").unwrap();
96+
import(&scan(), 3);
97+
assert_eq!(facts(), first);
98+
let preserved: Value = serde_json::from_slice(&fs::read(&fact_path).unwrap()).unwrap();
99+
assert_eq!(preserved["state"], "open");
100+
assert!(Path::new(&fact_path).is_file());
101+
let mut forged: Value = serde_json::from_slice(
102+
&fs::read(root.join(".codeguard/reports/clangdocplaceholder-1-1-3.json")).unwrap(),
103+
)
104+
.unwrap();
105+
forged["run_id"] = json!("clangdocplaceholder-1-1-4");
106+
let row = &forged["structure"]["observation"]["functions"][0];
107+
let bad_position = json!({"offset_byte":row["offset_byte"],"line":row["line"],"column_byte":row["column_byte"],"component":"parameter:foreign"});
108+
forged["placeholders"]["positions"] = json!([bad_position]);
109+
fs::write(
110+
root.join(".codeguard/reports/clangdocplaceholder-1-1-4.json"),
111+
serde_json::to_vec_pretty(&forged).unwrap(),
112+
)
113+
.unwrap();
114+
cli(vec![
115+
"work".into(),
116+
"sync".into(),
117+
root.display().to_string(),
118+
]);
119+
assert!(
120+
!root
121+
.join(".codeguard/state/consumed/clangdocplaceholder-1-1-4.json")
122+
.exists()
123+
);
124+
assert_eq!(facts(), first);
125+
let task_id = first[0].file_name().unwrap().to_str().unwrap();
126+
let verification = cli(vec![
127+
"task".into(),
128+
"verify".into(),
129+
task_id.into(),
130+
root.display().to_string(),
131+
]);
132+
assert_eq!(
133+
verification["reason"],
134+
"clang_placeholder_task_workflow_not_integrated"
135+
);
136+
assert!(verification["native_scan"].is_null());
137+
assert_eq!(verification["event_persisted"], false);
138+
}

‎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
@@ -1300,3 +1300,13 @@ Before exposing usable placeholder positions, the native probe SHALL validate th
13001300
#### Scenario: A forged return or parameter is inserted
13011301
- **WHEN** a placeholder position names a void return, unknown parameter, foreign source token or duplicate component
13021302
- **THEN** the validator SHALL reject it and the probe SHALL withdraw invalid placeholder observations instead of producing repair authority
1303+
1304+
### Requirement: Placeholder workbench import SHALL retain a separate stable open file task
1305+
1306+
work sync SHALL consume a separately versioned placeholder packet only after original native/structural shape, workspace/run identity, current tool/source association and placeholder component validation. File path, language, standard and placeholder policy SHALL define one stable task independent of source digest and line drift; this task SHALL have a distinct checker and rule identity from missing-component and native-warning tasks. Clean observations SHALL retain historical open facts. Forged component packets SHALL not obtain a consumption marker or new task. Until a dedicated candidate and original-task verifier exist, next/task verify SHALL report clang_placeholder_task_workflow_not_integrated without invoking another checker or recording a verification event. Public automatic persistence remains an uncompleted integration requirement.
1307+
1308+
#### Scenario: A placeholder moves and is later replaced by real documentation
1309+
- **WHEN** repeated current native observations move the function to another line, then report no placeholder positions
1310+
- **THEN** work sync SHALL reuse the original file task and keep its fact open rather than close on local absence
1311+
- **WHEN** a report inserts a foreign parameter component
1312+
- **THEN** work sync SHALL reject consumption and retain existing facts

‎openspec/changes/introduce-rust-codeguard-cli/tasks.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@
345345
- [ ] 8.26 实现 c 的 lint/comments 适配与规则;验收:真实工具正确样本和违规样本、错误配置/版本/报告反例通过,格式化不能冒充注释检查。
346346
- [ ] 8.27 完成 c 的 dependencies/CVE/security/build 适用能力及整体验收;验收:依赖生态映射和逐类别真实证据齐备,缺口未解决不升级 stable。
347347
- [ ] 8.28 为 cpp 固化六类别适用性、候选工具/方言/版本及缺口;验收:每个槽位有实际依据,not_applicable 不得用缺工具解释。
348-
- [ ] 8.29 实现 cpp 的 lint/comments 适配与规则;验收:真实工具正确样本和违规样本、错误配置/版本/报告反例通过,格式化不能冒充注释检查。 新增C++17局部原生/WASM对照:12例实际执行,包含2例语义边界;1测试通过、0忽略,未授予生产资格,见[验收记录](../../../tests/acceptance/cpp17-native-wasm-differential.md)。 编译数据库静态解析基础已补充:有界参数数组、原序多配置、命令字符串不执行及畸形/超限反例;2测试通过,已接到项目发现,记录双读稳定摘要与unknown配置;发现测试先失败后2项通过。尚未接到原生执行,不升级构建生态状态。 公开init定向4项默认/WASM均通过,补充六类参数上下文阻塞,涵盖静态摘要持久化、command-only阻塞及配置变化/删除时保留任务;见[初始化验收](../../../tests/acceptance/c-family-compilation-database-init.md)。 C++17类内普通方法及显式构造已有实际Clang结构观察,构造不要求返回说明;解析阶段已有2000声明/256参数预算,超限明确阻塞不截断。类/析构/模板等仍未解析,见[局部验收](../../../tests/acceptance/cpp17-method-documentation.md)。 成员文档公开CLI稳定任务和原Clang复检默认/WASM实际通过,修复仍保留未批准关闭状态,见[工作台验收](../../../tests/acceptance/cpp17-member-workbench.md)。;明确占位说明已有独立Clang AST适配器/封闭协议与2项测试(含真实原生),公开单文件comments已以0.10同次扫描反馈占位位置;稳定任务/原任务复检和共享check/hook尚未接入,见 tests/acceptance/clang-documentation-placeholders.md,父任务保持未完成。
348+
- [ ] 8.29 实现 cpp 的 lint/comments 适配与规则;验收:真实工具正确样本和违规样本、错误配置/版本/报告反例通过,格式化不能冒充注释检查。 新增C++17局部原生/WASM对照:12例实际执行,包含2例语义边界;1测试通过、0忽略,未授予生产资格,见[验收记录](../../../tests/acceptance/cpp17-native-wasm-differential.md)。 编译数据库静态解析基础已补充:有界参数数组、原序多配置、命令字符串不执行及畸形/超限反例;2测试通过,已接到项目发现,记录双读稳定摘要与unknown配置;发现测试先失败后2项通过。尚未接到原生执行,不升级构建生态状态。 公开init定向4项默认/WASM均通过,补充六类参数上下文阻塞,涵盖静态摘要持久化、command-only阻塞及配置变化/删除时保留任务;见[初始化验收](../../../tests/acceptance/c-family-compilation-database-init.md)。 C++17类内普通方法及显式构造已有实际Clang结构观察,构造不要求返回说明;解析阶段已有2000声明/256参数预算,超限明确阻塞不截断。类/析构/模板等仍未解析,见[局部验收](../../../tests/acceptance/cpp17-method-documentation.md)。 成员文档公开CLI稳定任务和原Clang复检默认/WASM实际通过,修复仍保留未批准关闭状态,见[工作台验收](../../../tests/acceptance/cpp17-member-workbench.md)。;明确占位说明已有独立Clang AST适配器/封闭协议与2项测试(含真实原生),公开单文件comments已以0.10同次扫描反馈占位位置;稳定任务/原任务复检和共享check/hook尚未接入,见 tests/acceptance/clang-documentation-placeholders.md,父任务保持未完成。;占位独立包work sync导入已验收默认/WASM稳定文件任务、消费收据、漂移复用、清空不关闭与伪造拒绝;公开自动持久化、next指引及专用verify仍未接入,返回具体未接入原因。
349349
- [ ] 8.30 完成 cpp 的 dependencies/CVE/security/build 适用能力及整体验收;验收:依赖生态映射和逐类别真实证据齐备,缺口未解决不升级 stable。
350350
- [ ] 8.31 为 objc 固化六类别适用性、候选工具/方言/版本及缺口;验收:每个槽位有实际依据,not_applicable 不得用缺工具解释。
351351
- [ ] 8.32 实现 objc 的 lint/comments 适配与规则;验收:真实工具正确样本和违规样本、错误配置/版本/报告反例通过,格式化不能冒充注释检查。

0 commit comments

Comments
 (0)