Skip to content

Commit 81900bf

Browse files
committed
验收 Ruby 六类别候选档案并完成 OpenSpec 8.16
1 parent 0614a44 commit 81900bf

18 files changed

Lines changed: 1472 additions & 1 deletion

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,3 +679,5 @@ Source builds add `codeguard lint ruby FILE.rb --ruby-tool /absolute/path/to/rub
679679
Ruby edit feedback: source builds of `hook execute` / `hook claude post-tool-use` accept `--ruby-tool ABS_PATH` or select Ruby from the caller's absolute PATH entries. Only confirmed edited files are parsed under the event deadline. Selected failures do not fall back; absence retains WASM candidates. Bounded dialogue carries current line positions and real stable tasks without source/tool-message echoes or invented columns, and asks agents to verify project Ruby compatibility first. `repair_ready` rechecks using the selected tool and retains real evidence references; zero diagnostics do not close tasks. New closed protocols are edit feedback 0.19 (inner 0.10) and recheck feedback 0.20 (inner 0.6); old schemas are preserved. Actual installed-host triggering, complete RuboCop coverage, and public npm release remain unverified.
680680

681681
Ruby version declaration guard: source builds read the nearest `.ruby-version` between the source and project root, bounded to 64 ancestor levels and 4096 bytes. Only `2.6.10`, `2.6.10p210` and their `ruby-` forms match the fixed parser. Other explicit versions yield `ruby_project_version_mismatch`; aliases/ambiguity yield `ruby_project_version_unresolved`; unsafe links/read failures yield `ruby_project_version_unreadable`. None starts the old Ruby or falls back to WASM. Declaration bytes and nearer missing declarations are rechecked after parsing; changes withdraw diagnostics. Saved positions are also withdrawn when a new declaration is incompatible. Standalone lint locates a `.codeguard` workspace before a Git root or Gemfile, so nested Gemfiles cannot hide the workspace pin; the nearest module version declaration remains authoritative. Absence retains an unapproved preliminary observation. Gemfile constraints, JRuby/RVM, complete project profiling and release remain pending. See [version guard acceptance](tests/acceptance/ruby-project-version.md).
682+
683+
The Ruby six-category candidate profile now records runtime dialects, project-lock version selection and conditional tool scopes. Native rules, full project coverage and platform qualification remain unverified. See [profile acceptance](tests/acceptance/ruby-candidate-baseline.md).

‎README.zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,3 +662,5 @@ codeguard task verify --help
662662
Ruby 编辑快检接线:源码构建的 `hook execute` / `hook claude post-tool-use` 可选择 `--ruby-tool ABS_PATH` 或调用方绝对 PATH 的 Ruby。只检查确认编辑的文件,共用事件截止时间;选定工具失败不回退,缺工具保留 WASM 候选。原生行号与稳定任务进入有界对话,不回显源码/工具消息或猜列号;先核对项目 Ruby 版本适用性。`repair_ready` 用同一工具复检并保存真实证据引用,零诊断仍不关闭任务。编辑反馈 0.19(内层 0.10)、复检反馈 0.20(内层 0.6)新增封闭 schema,旧协议不改。实际宿主自动触发、完整 RuboCop 与公开 npm 版本尚未验收。
663663

664664
Ruby 版本声明约束:源码构建在原生解析前有界读取源码祖先至项目根最近的 `.ruby-version`(最多 64 层、4096 字节),只接受 `2.6.10`、`2.6.10p210` 及对应 `ruby-` 前缀。其它明确版本返回 `ruby_project_version_mismatch`,别名/歧义返回 `ruby_project_version_unresolved`,链接或读取失败返回 `ruby_project_version_unreadable`;均不启动旧 Ruby,也不回退 WASM。解析前后复核声明字节及更近目录的缺项,变化撤回诊断;已保存诊断遇到不适用的新声明也会撤回。单文件入口优先定位 `.codeguard` 工作区、再定位 Git 根和 Gemfile,模块 Gemfile 不能遮蔽工作区版本,模块自身最近版本声明仍优先。无声明继续提供未批准的初步观察;Gemfile 运行时约束、JRuby/RVM、完整项目版本识别及发行仍待实现。见[版本约束验收](tests/acceptance/ruby-project-version.md)。
665+
666+
Ruby 六类别候选档案已独立固化运行时方言、项目锁版本策略和条件性工具范围;原生规则、完整项目和平台资格仍待验收。见[档案验收](tests/acceptance/ruby-candidate-baseline.md)。

‎crates/codeguard-adapters/src/lib.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ mod eslint_json;
7272
mod eslint_message_report;
7373
mod eslint_parsed;
7474
mod go_candidate;
75+
mod ruby_candidate;
76+
mod ruby_candidate_profile;
77+
mod ruby_candidate_slot;
78+
mod ruby_candidate_tool;
7579
mod grammar_asset_manifest;
7680
mod legacy_dylink_compat;
7781
mod zig_wasm_compat;
@@ -399,3 +403,8 @@ pub use kotlin_parsed::KotlinParsed;
399403

400404
mod kotlin_native_observation;
401405
pub use kotlin_native_observation::valid_kotlin_native_observation;
406+
407+
pub use ruby_candidate::{bundled_ruby_candidate_profile, parse_ruby_candidate_profile};
408+
pub use ruby_candidate_profile::RubyCandidateProfile;
409+
pub use ruby_candidate_slot::RubyCandidateSlot;
410+
pub use ruby_candidate_tool::RubyCandidateTool;
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//! Ruby 原生六类别研究档案的严格读者;不启动原生进程,也不注册已验收能力。
2+
use crate::{parse_unique_json, ruby_candidate_profile::RubyCandidateProfile};
3+
use codeguard_core::{CANDIDATE_PLATFORMS, CHECK_CATEGORIES};
4+
use std::collections::BTreeSet;
5+
6+
/// 读取内置 Ruby 档案并核对六类别、工具对应、方言、版本和条件;返回研究档案或错误。
7+
pub fn bundled_ruby_candidate_profile() -> Result<RubyCandidateProfile, String> {
8+
parse_ruby_candidate_profile(include_str!(
9+
"../../../rulepacks/ruby_static_candidate_v1.json"
10+
))
11+
}
12+
/// 消费外部候选 JSON;最多 64KiB,拒绝重复键、未知字段、类别缺项及虚报能力。
13+
pub fn parse_ruby_candidate_profile(raw: &str) -> Result<RubyCandidateProfile, String> {
14+
if raw.len() > 64 * 1024 {
15+
return Err("Ruby 候选档案超出字节预算".into());
16+
}
17+
let value = parse_unique_json(raw.as_bytes()).map_err(str::to_string)?;
18+
let profile: RubyCandidateProfile = serde_json::from_value(value).map_err(|e| e.to_string())?;
19+
if profile.schema_version != "1.0.0"
20+
|| profile.language != "ruby"
21+
|| profile.runtime_validation != "unverified"
22+
|| profile.platform_validation != "unverified"
23+
|| !exact_set(&profile.runtime_dialects, &["mri", "jruby", "truffleruby"])
24+
|| !exact_set(&profile.platforms, &CANDIDATE_PLATFORMS)
25+
|| !exact_set(
26+
&profile.target_version_inputs,
27+
&[
28+
".ruby-version",
29+
".tool-versions",
30+
"mise.toml",
31+
"Gemfile",
32+
"Gemfile.lock",
33+
"*.gemspec",
34+
".rubocop.yml",
35+
],
36+
)
37+
|| !valid_gaps(&profile.version_gaps)
38+
{
39+
return Err("Ruby 档案版本、方言、版本输入或平台集合未验证/缺失".into());
40+
}
41+
let names: Vec<String> = profile
42+
.categories
43+
.iter()
44+
.map(|s| s.category.clone())
45+
.collect();
46+
if !exact_set(&names, &CHECK_CATEGORIES) {
47+
return Err("Ruby 档案必须恰有六个不重复类别".into());
48+
}
49+
for slot in &profile.categories {
50+
let expected: &[&str] = match slot.category.as_str() {
51+
"lint" => &["rubocop"],
52+
"comments" => &["rubocop_documentation", "rubocop_documentation_method"],
53+
"dependencies" => &["bundle_list"],
54+
"cve" => &["bundler_audit"],
55+
"security" => &["rubocop_security", "brakeman"],
56+
"build" => &["gem_build"],
57+
_ => return Err("Ruby 类别未知".into()),
58+
};
59+
let ids: Vec<String> = slot.tools.iter().map(|t| t.tool_id.clone()).collect();
60+
if slot.status != "gap"
61+
|| !exact_set(&ids, expected)
62+
|| slot.applicability
63+
!= if slot.category == "build" {
64+
"project_dependent"
65+
} else {
66+
"applicable"
67+
}
68+
|| slot.requires_database_freshness != (slot.category == "cve")
69+
|| !valid_gaps(&slot.coverage_gaps)
70+
{
71+
return Err(format!(
72+
"Ruby {} 的类别适用性、工具或缺口无效",
73+
slot.category
74+
));
75+
}
76+
for tool in &slot.tools {
77+
let (argv, scope): (&[&str], &str) = match tool.tool_id.as_str() {
78+
"rubocop" => (
79+
&["bundle", "exec", "rubocop", "--format", "json"],
80+
"ruby_project",
81+
),
82+
"rubocop_documentation" => (
83+
&[
84+
"bundle",
85+
"exec",
86+
"rubocop",
87+
"--only",
88+
"Style/Documentation",
89+
"--format",
90+
"json",
91+
],
92+
"ruby_project",
93+
),
94+
"rubocop_documentation_method" => (
95+
&[
96+
"bundle",
97+
"exec",
98+
"rubocop",
99+
"--only",
100+
"Style/DocumentationMethod",
101+
"--format",
102+
"json",
103+
],
104+
"ruby_project",
105+
),
106+
"bundle_list" => (&["bundle", "list"], "bundler_project"),
107+
"bundler_audit" => (
108+
&["bundle-audit", "check", "--no-update", "--format", "json"],
109+
"bundler_project",
110+
),
111+
"rubocop_security" => (
112+
&[
113+
"bundle", "exec", "rubocop", "--only", "Security", "--format", "json",
114+
],
115+
"ruby_project",
116+
),
117+
"brakeman" => (
118+
&[
119+
"bundle",
120+
"exec",
121+
"brakeman",
122+
"--format",
123+
"json",
124+
"--no-pager",
125+
],
126+
"rails_project",
127+
),
128+
"gem_build" => (&["gem", "build", "SELECTED_GEMSPEC"], "gem_project"),
129+
_ => return Err("Ruby 工具未知".into()),
130+
};
131+
if tool.candidate_version != "project_locked"
132+
|| tool.version_status != "unvalidated_project_lock_required"
133+
|| tool.scope != scope
134+
|| tool
135+
.candidate_argv
136+
.iter()
137+
.map(String::as_str)
138+
.collect::<Vec<_>>()
139+
!= argv
140+
|| tool.sources.is_empty()
141+
|| tool.sources.len() > 8
142+
|| tool.sources.iter().any(|s| {
143+
!s.starts_with("https://")
144+
|| s.len() > 512
145+
|| s.bytes().any(|b| b.is_ascii_whitespace())
146+
})
147+
{
148+
return Err(format!(
149+
"Ruby {} 的版本、命令、范围或来源无效",
150+
tool.tool_id
151+
));
152+
}
153+
}
154+
}
155+
Ok(profile)
156+
}
157+
fn exact_set(actual: &[String], expected: &[&str]) -> bool {
158+
actual.len() == expected.len()
159+
&& actual.iter().map(String::as_str).collect::<BTreeSet<_>>()
160+
== expected.iter().copied().collect()
161+
}
162+
fn valid_gaps(gaps: &[String]) -> bool {
163+
!gaps.is_empty()
164+
&& gaps.len() <= 32
165+
&& gaps.iter().collect::<BTreeSet<_>>().len() == gaps.len()
166+
&& gaps.iter().all(|s| {
167+
!s.is_empty()
168+
&& s.len() <= 256
169+
&& s.bytes()
170+
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
171+
})
172+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::ruby_candidate_slot::RubyCandidateSlot;
2+
use serde::Deserialize;
3+
/// Ruby 运行时方言、版本输入和六类别候选档案;对应 OpenSpec 8.16。
4+
#[derive(Clone, Debug, Deserialize)]
5+
#[serde(deny_unknown_fields)]
6+
pub struct RubyCandidateProfile {
7+
/// 版本化档案协议。
8+
pub schema_version: String,
9+
/// 固定语言 ID。
10+
pub language: String,
11+
/// 需要分别验证的运行时,不证明当前原生探针支持这些运行时。
12+
pub runtime_dialects: Vec<String>,
13+
/// 运行时组合尚未验收。
14+
pub runtime_validation: String,
15+
/// 候选平台集合。
16+
pub platforms: Vec<String>,
17+
/// 平台验收状态。
18+
pub platform_validation: String,
19+
/// 版本约束可能来自这些输入;此档案不读取或执行它们。
20+
pub target_version_inputs: Vec<String>,
21+
/// 版本兼容性剩余缺口。
22+
pub version_gaps: Vec<String>,
23+
/// 六类别的完整候选集合。
24+
pub categories: Vec<RubyCandidateSlot>,
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use crate::ruby_candidate_tool::RubyCandidateTool;
2+
use serde::Deserialize;
3+
/// Ruby 六类别适用性和缺口;对应 OpenSpec 8.16,缺工具不等于类别不适用。
4+
#[derive(Clone, Debug, Deserialize)]
5+
#[serde(deny_unknown_fields)]
6+
pub struct RubyCandidateSlot {
7+
/// 检查类别。
8+
pub category: String,
9+
/// 全语言适用或需项目构建目标判定。
10+
pub applicability: String,
11+
/// 完整能力验收仍缺失。
12+
pub status: String,
13+
/// 有明确范围限制的原生工具候选。
14+
pub tools: Vec<RubyCandidateTool>,
15+
/// CVE 必须单独核验数据库身份和时效。
16+
pub requires_database_freshness: bool,
17+
/// 尚未验收的配置、报告、身份及覆盖条件。
18+
pub coverage_gaps: Vec<String>,
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use serde::Deserialize;
2+
/// Ruby 原生候选工具契约;对应 OpenSpec 8.16,仅供研究,不可执行或签发能力。
3+
#[derive(Clone, Debug, Deserialize)]
4+
#[serde(deny_unknown_fields)]
5+
pub struct RubyCandidateTool {
6+
/// 原生工具和检查族的固定标识。
7+
pub tool_id: String,
8+
/// 需由项目锁解析的版本,尚未通过制品及兼容性验收。
9+
pub candidate_version: String,
10+
/// 候选版本状态。
11+
pub version_status: String,
12+
/// 研究命令示例;包含占位符时必须由未来批准计划显式绑定。
13+
pub candidate_argv: Vec<String>,
14+
/// Ruby、Bundler、Rails、YARD 或 gem 项目的具体适用条件。
15+
pub scope: String,
16+
/// 核对过的原生一手文档。
17+
pub sources: Vec<String>,
18+
}

0 commit comments

Comments
 (0)