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
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: codeguard

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
gate:
# ubuntu-latest 预装稳定版 Rust;Cargo.toml 声明 rust-version = 1.85 / edition = 2024。
runs-on: ubuntu-latest
Comment on lines +13 to +14

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 Test against the declared Rust minimum version

The repository declares Rust 1.85 as its minimum, but this job uses whichever newer stable toolchain happens to be preinstalled on ubuntu-latest. A change using an API or language feature introduced after 1.85 will therefore pass CI while failing for users on the supported minimum compiler; install and select Rust 1.85 for at least one build/test job.

Useful? React with 👍 / 👎.

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Check out corpus evidence source
# 固定语料按 sha256 校验来自插件仓的真实文件(provenance.source_ref =
# codeguard-plugin@9e4adb1),因此必须把该 commit 检出到兄弟目录;
# 只建空目录不够,会因摘要不匹配而失败。
run: |
git clone --quiet https://github.com/full-stack-plugins/codeguard-plugin.git ../codeguard-plugin
git -C ../codeguard-plugin checkout --quiet 9e4adb1

- name: Verify crate layering debt does not grow
run: python3 scripts/check_layering.py

- name: Build workspace
run: cargo build --workspace --all-targets

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 Enforce the committed lockfile in CI

When a PR changes Cargo.toml but omits the corresponding Cargo.lock update, this command may resolve dependencies and rewrite the lockfile only inside the ephemeral runner, allowing CI to pass even though the repository's documented locked build/test commands will fail for users. The local cargo build --help describes --locked as “Assert that Cargo.lock will remain unchanged”; add it here so CI detects stale lockfiles.

Useful? React with 👍 / 👎.


- name: Run test suite
run: cargo test --workspace --all-targets

- name: Explain remaining layering debt
if: always()
run: python3 scripts/check_layering.py --explain
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! 编译发现身份绑定原生源码范围;不靠诊断顺序给其它问题续用身份。

use codeguard_adapters::CargoBuildDiagnostic;
use crate::CargoBuildDiagnostic;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! 原生 ESLint 发现到脱敏任务输入的稳定投影;不赋予策略或报告来源权威。
use codeguard_adapters::EslintDiagnostic;
use crate::EslintDiagnostic;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, path::Path};
Expand Down Expand Up @@ -103,7 +103,7 @@ pub fn project_eslint_findings(
}

/// 复核脱敏任务输入的稳定投影;消息摘要只验证形状,不自证原生来源。
pub(crate) fn validate_records(
pub fn validate_records(
relative: &str,
absolute: &str,
source: &[u8],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Go 原生诊断的局部稳定身份;行号用于定位,源码字节摘要用于本轮归属复核。

use codeguard_adapters::GoVetFinding;
use crate::GoVetFinding;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};

Expand Down
8 changes: 8 additions & 0 deletions crates/codeguard-adapters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,11 @@ mod tests {
assert!(parse_legacy_registry(r#"{"languages":[]}"#).is_err());
}
}
mod cargo_build_finding_identity;
pub use cargo_build_finding_identity::cargo_build_finding_record;
mod eslint_finding_identity;
pub use eslint_finding_identity::{project_eslint_findings, validate_records};
mod go_finding_identity;
pub use go_finding_identity::go_finding_record;
mod rustdoc_finding_identity;
pub use rustdoc_finding_identity::rustdoc_finding_record;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! 文档发现身份绑定原生源码范围;不靠诊断顺序给其它问题续用身份。

use codeguard_adapters::RustdocFinding;
use crate::RustdocFinding;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};

Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/eslint_workbench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! ESLint 局部原生观察到既有修复队列的连接;没有审批或关闭权威。
use codeguard_adapters::project_eslint_findings;
use crate::{
eslint_finding_identity::project_eslint_findings,
eslint_lint_arguments::EslintLintArguments,
eslint_probe_request::EslintProbeRequest,
eslint_probe_result::EslintProbeResult,
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/go_lint_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::check_budget::parse_check_timeout;
use crate::discovery::discover;
use crate::go_finding_identity::go_finding_record;
use codeguard_adapters::go_finding_record;
use crate::work_sync::{save_local_report, sync_local_workspace};
use crate::workspace_refresh::read_workspace_baseline;
use codeguard_adapters::{GoVetParseState, legacy_registry, parse_go_vet_json};
Expand Down
4 changes: 0 additions & 4 deletions crates/codeguard-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ mod eslint_directory;
mod eslint_discovery;
#[cfg(unix)]
mod eslint_effective_settings;
pub mod eslint_finding_identity;
#[cfg(unix)]
mod eslint_lint_arguments;
#[cfg(unix)]
Expand Down Expand Up @@ -71,7 +70,6 @@ pub use signed_approval_verifier::verify_signed_approval;
pub use signed_prior_approval_input::SignedPriorApprovalInput;
pub use verified_approval_snapshot::VerifiedApprovalSnapshot;
pub mod cargo_audit_command;
pub mod cargo_build_finding_identity;
pub mod cargo_build_repair_brief;
#[cfg(unix)]
pub mod check_budget;
Expand All @@ -95,7 +93,6 @@ pub mod false_positive_decision;
pub mod git_index_safety;
#[cfg(unix)]
pub mod git_index_safety_command;
pub mod go_finding_identity;
#[cfg(unix)]
pub mod go_lint_command;
pub mod init_command;
Expand Down Expand Up @@ -154,7 +151,6 @@ pub mod rust_comments_command;
pub(crate) mod rust_cve_task_recheck;
#[cfg(unix)]
pub mod rust_lint_scan;
pub mod rustdoc_finding_identity;
pub mod rustdoc_repair_brief;
#[cfg(unix)]
pub(crate) mod rustdoc_task_recheck;
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/rust_build_command.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! 原生 Cargo/check 类型检查观察;完整项目策略与修复任务仍独立核验。

use crate::cargo_build_finding_identity::cargo_build_finding_record;
use codeguard_adapters::cargo_build_finding_record;
use crate::cargo_build_repair_brief::cargo_build_repair_brief;
use crate::check_budget::{
budget_record, parse_check_timeout, resolve_project_default, select_check_timeout,
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/rust_comments_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::check_budget::{
};
use crate::discovery::discover;
use crate::doctor_scratch::DoctorScratch;
use crate::rustdoc_finding_identity::rustdoc_finding_record;
use codeguard_adapters::rustdoc_finding_record;
use crate::rustdoc_repair_brief::rustdoc_repair_brief;
use crate::work_sync::{save_local_report, sync_local_workspace};
use crate::workspace_refresh::read_workspace_baseline;
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/work_sync/eslint_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ pub(super) fn parse(
{
return Err("eslint_completion_invalid");
}
if !crate::eslint_finding_identity::validate_records(
if !codeguard_adapters::validate_records(
relative,
absolute,
&bytes["source"],
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/work_sync/rust_build_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::{
BlockerInput, FindingInput, MAX_REPORT_BYTES, ReportInput, read_bounded_file, safe_reason,
safe_relative_path, safe_run_id, valid_sha256,
};
use crate::cargo_build_finding_identity::cargo_build_finding_record;
use codeguard_adapters::cargo_build_finding_record;
use crate::cargo_build_repair_brief::cargo_build_repair_brief;
use codeguard_adapters::CargoBuildDiagnostic;
use codeguard_runtime::SourceSnapshot;
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/src/work_sync/rustdoc_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::{
BlockerInput, FindingInput, MAX_REPORT_BYTES, ReportInput, read_bounded_file, safe_reason,
safe_relative_path, safe_run_id, valid_sha256,
};
use crate::rustdoc_finding_identity::rustdoc_finding_record;
use codeguard_adapters::rustdoc_finding_record;
use crate::rustdoc_repair_brief::rustdoc_repair_brief;
use codeguard_adapters::RustdocFinding;
use serde_json::{Value, json};
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/tests/eslint_finding_identity.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use codeguard_adapters::EslintDiagnostic;
use codeguard_cli::eslint_finding_identity::project_eslint_findings;
use codeguard_adapters::project_eslint_findings;
fn finding(line: u32, column: u32) -> EslintDiagnostic {
EslintDiagnostic {
path: "/fixture/app.js".into(),
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/tests/go_finding_identity.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use codeguard_adapters::GoVetFinding;
use codeguard_cli::go_finding_identity::go_finding_record;
use codeguard_adapters::go_finding_record;

fn finding(line: u32) -> GoVetFinding {
GoVetFinding {
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/tests/go_work_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ fn real_go_findings_sync_without_duplicate_tasks_or_false_closure() {

fn synthetic_report(p: &Project) -> Value {
use codeguard_adapters::GoVetFinding;
use codeguard_cli::go_finding_identity::go_finding_record;
use codeguard_adapters::go_finding_record;
use sha2::{Digest, Sha256};
let mut report = p.lint(None);
report["run_id"] = serde_json::json!("synthetic-go");
Expand Down
2 changes: 1 addition & 1 deletion crates/codeguard-cli/tests/rustdoc_identity_contract.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use codeguard_adapters::parse_cargo_rustdoc_json;
use codeguard_cli::rustdoc_finding_identity::rustdoc_finding_record;
use codeguard_adapters::rustdoc_finding_record;
use serde_json::json;

fn record(source: &[u8], start: u64, end: u64) -> serde_json::Value {
Expand Down
19 changes: 19 additions & 0 deletions layer-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"accepted": {
"cargo_module_graph.rs": "Cargo 模块图领域模型;目标 core",
"checkstyle_probe.rs": "包裹 checkstyle 原生调用;目标 adapters,随 checkstyle 适配迁移",
"eslint_probe.rs": "包裹 eslint 原生调用与配置解析;目标 adapters,随 eslint 适配迁移",
"maven_dependency_probe.rs": "包裹 OWASP dependency-check;目标 adapters",
"maven_javadoc_probe.rs": "包裹 Maven Javadoc 生命周期;目标 adapters",
"maven_probe.rs": "包裹 Maven 原生调用;目标 adapters,随 Maven 适配迁移",
"module_graph.rs": "模块图领域模型;目标 core,与 core 既有 task_graph 同类",
"npm_audit_probe.rs": "包裹 npm audit 原生调用;目标 adapters",
"npm_input_state.rs": "npm 输入状态领域模型;目标 core",
"owasp_maven_probe.rs": "包裹 OWASP Maven 报告解析;目标 adapters",
"pmd6_probe.rs": "包裹 PMD6 原生调用;目标 adapters,随 PMD 适配迁移",
"ruff_probe.rs": "包裹 ruff 原生调用与配置观察;目标 adapters,随 ruff 适配迁移",
"tool_identity.rs": "工具身份与版本绑定;目标 adapters,core 已有 native_checker_binding 可对齐"
},
"note": "codeguard-cli 中的已知分层债务。只减不增:模块下移后必须同步移除本文件登记。",
"version": 1
}
158 changes: 158 additions & 0 deletions scripts/check_layering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""分层门禁:阻止领域逻辑继续堆积在 codeguard-cli 这个二进制 crate。

背景:codeguard 把执行内核切成四个 crate,设计意图是
core(契约与领域类型)← runtime(执行)← adapters(工具适配)← cli(组装)
但实际上一度出现倒挂——adapters 已有的工具诊断类型,其身份映射逻辑却住在 cli,
让 cli 膨胀到远大于 core,且无法单独测试领域规则。

本门禁不试图一次性搬完(那会让 diff 无法审阅),而是把「倒挂」变成显式债务:
cli 中仍然存在的领域形态模块被登记在 layer-baseline.json 里,数量只减不增。
新增领域模块而不同步下移,会让本检查失败并指出应该去哪个 crate。

用法:
python3 scripts/check_layering.py # 检查
python3 scripts/check_layering.py --explain # 打印每个模块的归属理由
python3 scripts/check_layering.py --accept <name> # 登记一个既有模块(带理由)
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CLI_SRC = ROOT / "crates" / "codeguard-cli" / "src"
BASELINE = ROOT / "layer-baseline.json"

# 领域形态:这些后缀的模块在 cli 中几乎都属于"工具适配 / 报告构造 / 领域类型",
# 而不是命令行管道。cli 只应保留参数解析、终端输出与编排。
DOMAIN_SHAPED = (
"_probe",
"_identity",
"_observation",
"_diagnostic",
"_parsed",
"_config",
"_graph",
"_state",
"_model",
)

# 每个后缀应归属的 crate,用于给出可执行的迁移建议。
HOME_FOR = {
"_probe": "codeguard-adapters",
"_identity": "codeguard-adapters",
"_diagnostic": "codeguard-adapters",
"_parsed": "codeguard-adapters",
"_config": "codeguard-adapters",
"_observation": "codeguard-adapters",
"_graph": "codeguard-core",
"_state": "codeguard-core",
"_model": "codeguard-core",
}

# 命令入口与管道是 cli 的正当职责,不计入债务。
CLI_ALLOWED = ("_command", "_arguments", "_preview", "_brief", "_feedback", "main.rs", "lib.rs")


def domain_modules() -> dict[str, str]:
"""返回 {模块名: 应归属 crate},只含 cli 中本应下移的领域模块。"""
found: dict[str, str] = {}
for path in sorted(CLI_SRC.rglob("*.rs")):
rel = path.relative_to(CLI_SRC).as_posix()
if rel in CLI_ALLOWED or any(rel.endswith(a) for a in CLI_ALLOWED):
continue
stem = rel[:-3]
if stem.endswith(CLI_ALLOWED[:-1]) or stem in ("main", "lib"):
continue
for suffix, home in HOME_FOR.items():
if stem.endswith(suffix):
Comment on lines +68 to +72

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 Inspect directory module names when enforcing layering

When a conventional directory module such as src/foo_identity/mod.rs is added and declared from lib.rs, stem becomes foo_identity/mod, so none of the suffix checks match; lib.rs is also explicitly skipped. I verified that adding this structure leaves the gate reporting “Layering OK,” allowing exactly the new adapter-shaped CLI module this check is intended to reject. Inspect the parent directory name when the file is mod.rs.

Useful? React with 👍 / 👎.

found[rel] = home
break
return found


def load_baseline() -> dict:
if not BASELINE.is_file():
return {"version": 1, "accepted": {}}
return json.loads(BASELINE.read_text(encoding="utf-8"))


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="codeguard crate 分层门禁")
parser.add_argument("--explain", action="store_true", help="打印每个模块的归属理由")
parser.add_argument("--accept", metavar="NAME", help="把一个既有模块登记为已知债务")
parser.add_argument("--reason", metavar="TEXT", help="配合 --accept 记录理由")
args = parser.parse_args(argv)

current = domain_modules()
baseline = load_baseline()
accepted: dict = baseline.get("accepted", {})

if args.accept:
if args.accept not in current:
print(f"{args.accept} 不在当前 cli 领域模块清单中,无需登记", file=sys.stderr)
return 2
accepted[args.accept] = args.reason or "未记录理由"
baseline["accepted"] = accepted
BASELINE.write_text(
json.dumps(baseline, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"已登记 {args.accept} → {HOME_FOR.get(suffix_of(args.accept), 'codeguard-core')}")
return 0

if args.explain:
for name in sorted(current):
state = "已知债务" if name in accepted else "未登记(会被本检查拒绝)"
print(f"{name:52} → {current[name]:20} [{state}]")
if name in accepted:
print(f"{'':52} 理由:{accepted[name]}")
return 0

# 未登记的新领域模块 = 倒挂在扩大
undeclared = sorted(set(current) - set(accepted))

if undeclared:
print("分层门禁失败:以下领域形态模块新增在 codeguard-cli 中。", file=sys.stderr)
print("它们应随工具适配或领域契约下移,而不是留在二进制 crate:\n", file=sys.stderr)
for name in undeclared:
print(f" {name} → 应归属 {current[name]}", file=sys.stderr)
print(
"\n处理方式:把模块移到对应 crate 并更新调用方;"
"若确有理由暂留,用 --accept 显式登记为已知债务。",
file=sys.stderr,
)
return 1

# 已登记模块被移走了:基线应收紧,不留空壳额度
removed = sorted(set(accepted) - set(current))
stale = [n for n in removed if n in accepted]

if stale:
print("分层门禁失败:以下已登记模块已不存在,请从 layer-baseline.json 移除,", file=sys.stderr)
print("以便额度随搬移自动收紧:\n", file=sys.stderr)
for name in stale:
print(f" {name}", file=sys.stderr)
return 1

print(
f"Layering OK: codeguard-cli 中已知领域债务 {len(current)} 个"
f"(全部已登记,无新增);core/adapters 未被 CLI 逻辑反向污染。"
)
return 0


def suffix_of(name: str) -> str:
stem = name[:-3] if name.endswith(".rs") else name
for suffix in HOME_FOR:
if stem.endswith(suffix):
return suffix
return ""


if __name__ == "__main__":
raise SystemExit(main())
Loading