Files

16 KiB
Raw Permalink Blame History

设计文档:仓库治理只读审计

概述

本设计描述三个 Python 审计脚本的实现方案,用于对 etl-billiards 仓库进行只读分析并生成三份 Markdown 报告。脚本仅读取文件系统和源代码,不连接数据库、不修改任何现有文件,仅在 docs/audit/ 目录下输出报告。

审计脚本采用模块化设计:一个共享的仓库扫描器负责遍历文件系统,三个独立的分析器分别生成文件清单、流程树和文档对齐报告。

架构

graph TD
    A[scripts/audit/run_audit.py<br/>审计主入口] --> B[scripts/audit/scanner.py<br/>仓库扫描器]
    A --> C[scripts/audit/inventory_analyzer.py<br/>文件清单分析器]
    A --> D[scripts/audit/flow_analyzer.py<br/>流程树分析器]
    A --> E[scripts/audit/doc_alignment_analyzer.py<br/>文档对齐分析器]
    
    B --> F[文件系统<br/>只读遍历]
    C --> G[docs/audit/file_inventory.md]
    D --> H[docs/audit/flow_tree.md]
    E --> I[docs/audit/doc_alignment.md]
    
    C --> B
    D --> B
    E --> B

执行流程

  1. run_audit.py 作为主入口,初始化扫描器并依次调用三个分析器
  2. scanner.py 递归遍历仓库,构建文件元信息列表(路径、大小、类型)
  3. 各分析器接收扫描结果,执行各自的分析逻辑,输出 Markdown 报告
  4. 所有报告写入 docs/audit/ 目录

组件与接口

1. 仓库扫描器 (scripts/audit/scanner.py)

负责递归遍历仓库文件系统,返回结构化的文件元信息。

@dataclass
class FileEntry:
    """单个文件/目录的元信息"""
    rel_path: str          # 相对于仓库根目录的路径
    is_dir: bool           # 是否为目录
    size_bytes: int        # 文件大小(目录为 0
    extension: str         # 文件扩展名(小写,含点号)
    is_empty_dir: bool     # 是否为空目录

EXCLUDED_PATTERNS: list[str] = [
    ".git", "__pycache__", ".pytest_cache",
    "*.pyc", ".kiro",
]

def scan_repo(root: Path, exclude: list[str] = EXCLUDED_PATTERNS) -> list[FileEntry]:
    """递归扫描仓库,返回所有文件和目录的元信息列表"""
    ...

2. 文件清单分析器 (scripts/audit/inventory_analyzer.py)

对扫描结果进行用途分类和处置标签分配。

# 用途分类枚举
class Category(str, Enum):
    CORE_CODE = "核心代码"
    CONFIG = "配置"
    DATABASE_DEF = "数据库定义"
    TEST = "测试"
    DOCS = "文档"
    SCRIPTS = "脚本工具"
    GUI = "GUI"
    BUILD_DEPLOY = "构建与部署"
    LOG_OUTPUT = "日志与输出"
    TEMP_DEBUG = "临时与调试"
    OTHER = "其他"

# 处置标签枚举
class Disposition(str, Enum):
    KEEP = "保留"
    CANDIDATE_DELETE = "候选删除"
    CANDIDATE_ARCHIVE = "候选归档"
    NEEDS_REVIEW = "待确认"

@dataclass
class InventoryItem:
    """清单条目"""
    rel_path: str
    category: Category
    disposition: Disposition
    description: str

def classify(entry: FileEntry) -> InventoryItem:
    """根据路径、扩展名等规则对单个文件/目录进行分类和标签分配"""
    ...

def build_inventory(entries: list[FileEntry]) -> list[InventoryItem]:
    """批量分类所有文件条目"""
    ...

def render_inventory_report(items: list[InventoryItem], repo_root: str) -> str:
    """生成 Markdown 格式的文件清单报告"""
    ...

分类规则(按优先级从高到低)

路径模式 用途分类 默认处置
tmp/ 下所有文件 临时与调试 候选删除/候选归档
logs/export/ 下的运行时产出 日志与输出 候选归档
*.lnk*.rar 文件 其他 候选删除
空目录(如 Deleded & backup/ 其他 候选删除
tasks/loaders/scd/orchestration/quality/models/utils/api/ 核心代码 保留
config/ 配置 保留
database/*.sqldatabase/migrations/ 数据库定义 保留
database/*.py 核心代码 保留
tests/ 测试 保留
docs/ 文档 保留
scripts/ 下的 .py 文件 脚本工具 保留/待确认
gui/ GUI 保留
setup.pybuild_exe.py*.bat*.sh*.ps1 构建与部署 保留
根目录散落文件(Prompt用.mdUntitledfix_symbols.py 等) 其他 待确认

3. 流程树分析器 (scripts/audit/flow_analyzer.py)

通过静态分析 Python 源码的 import 语句和类继承关系,构建从入口到末端模块的调用树。

@dataclass
class FlowNode:
    """流程树节点"""
    name: str              # 节点名称(模块名/类名/函数名)
    source_file: str       # 所在源文件路径
    node_type: str         # 类型entry/module/class/function
    children: list["FlowNode"]

def parse_imports(filepath: Path) -> list[str]:
    """使用 ast 模块解析 Python 文件的 import 语句,返回被导入的本地模块列表"""
    ...

def build_flow_tree(repo_root: Path, entry_file: str) -> FlowNode:
    """从指定入口文件出发,递归追踪 import 链,构建流程树"""
    ...

def find_orphan_modules(repo_root: Path, all_entries: list[FileEntry], reachable: set[str]) -> list[str]:
    """找出未被任何入口直接或间接引用的 Python 模块"""
    ...

def render_flow_report(trees: list[FlowNode], orphans: list[str], repo_root: str) -> str:
    """生成 Markdown 格式的流程树报告(含 Mermaid 图和缩进文本)"""
    ...

入口点识别

  • CLI 入口:cli/main.pymain() 函数
  • GUI 入口:gui/main.pymain() 函数
  • 批处理入口:run_etl.batrun_gui.batrun_ods.bat → 解析其中的 python 命令
  • 运维脚本:scripts/*.py → 各自的 if __name__ == "__main__"

静态分析策略

  • 使用 Python ast 模块解析源文件,提取 importfrom ... import 语句
  • 仅追踪项目内部模块(排除标准库和第三方包)
  • 通过 orchestration/task_registry.py 的注册语句识别所有任务类及其源文件
  • 通过类继承关系(BaseTaskBaseLoaderBaseDwsTask 等)识别任务和加载器层级

4. 文档对齐分析器 (scripts/audit/doc_alignment_analyzer.py)

检查文档与代码之间的映射关系、过期点、冲突点和缺失点。

@dataclass
class DocMapping:
    """文档与代码的映射关系"""
    doc_path: str          # 文档文件路径
    doc_topic: str         # 文档主题
    related_code: list[str]  # 关联的代码文件/模块
    status: str            # 状态aligned/stale/conflict/orphan

@dataclass
class AlignmentIssue:
    """对齐问题"""
    doc_path: str
    issue_type: str        # stale/conflict/missing
    description: str
    related_code: str

def scan_docs(repo_root: Path) -> list[str]:
    """扫描所有文档文件路径"""
    ...

def extract_code_references(doc_path: Path) -> list[str]:
    """从文档中提取代码引用(文件路径、类名、函数名、表名等)"""
    ...

def check_reference_validity(ref: str, repo_root: Path) -> bool:
    """检查文档中的代码引用是否仍然有效"""
    ...

def find_undocumented_modules(repo_root: Path, documented: set[str]) -> list[str]:
    """找出缺少文档的核心代码模块"""
    ...

def check_ddl_vs_dictionary(repo_root: Path) -> list[AlignmentIssue]:
    """比对 DDL 文件与数据字典文档的覆盖度"""
    ...

def check_api_samples_vs_parsers(repo_root: Path) -> list[AlignmentIssue]:
    """比对 API 响应样本与 ODS 表结构/解析器的一致性"""
    ...

def render_alignment_report(mappings: list[DocMapping], issues: list[AlignmentIssue], repo_root: str) -> str:
    """生成 Markdown 格式的文档对齐报告"""
    ...

文档来源识别

  • docs/ 目录下的 .md.txt.csv 文件
  • 根目录的 README.md
  • 开发笔记/ 目录
  • 各模块内的 README.mdgui/README.mdfetch-test/README.md
  • .kiro/steering/ 下的引导文件
  • docs/test-json-doc/ 下的 API 响应样本及分析文档

对齐检查策略

  • 过期点检测:文档中引用的文件路径、类名、函数名在代码中已不存在
  • 冲突点检测DDL 中的表/字段定义与数据字典文档不一致API 样本字段与解析器不匹配
  • 缺失点检测:核心代码模块(tasks/loaders/orchestration/ 等)缺少对应文档

5. 审计主入口 (scripts/audit/run_audit.py)

def run_audit(repo_root: Path | None = None) -> None:
    """执行完整审计流程,生成三份报告到 docs/audit/"""
    ...

if __name__ == "__main__":
    run_audit()

数据模型

FileEntry文件元信息

字段 类型 说明
rel_path str 相对路径
is_dir bool 是否为目录
size_bytes int 文件大小
extension str 扩展名
is_empty_dir bool 是否为空目录

InventoryItem清单条目

字段 类型 说明
rel_path str 相对路径
category Category 用途分类
disposition Disposition 处置标签
description str 简要说明

FlowNode流程树节点

字段 类型 说明
name str 节点名称
source_file str 源文件路径
node_type str 节点类型
children list[FlowNode] 子节点列表

DocMapping / AlignmentIssue文档对齐

字段 类型 说明
doc_path str 文档路径
doc_topic / issue_type str 主题/问题类型
related_code list[str] / str 关联代码
status / description str 状态/描述

正确性属性

属性Property是指在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是连接人类可读规格说明与机器可验证正确性保证之间的桥梁。

Property 1: classify 完整性

对于任意 FileEntryclassify 函数返回的 InventoryItemcategory 字段应属于 Category 枚举,disposition 字段应属于 Disposition 枚举,且 description 字段为非空字符串。

Validates: Requirements 1.2, 1.3

Property 2: 清单渲染完整性

对于任意 InventoryItem 列表,render_inventory_report 生成的 Markdown 文本中,每个条目对应的行应包含该条目的 rel_pathcategory.valuedisposition.valuedescription 四个字段。

Validates: Requirements 1.4

Property 3: 空目录标记为候选删除

对于任意 is_empty_dir=TrueFileEntryclassify 返回的 disposition 应为 Disposition.CANDIDATE_DELETE

Validates: Requirements 1.5

Property 4: .lnk/.rar 文件标记为候选删除

对于任意 扩展名为 .lnk.rarFileEntryclassify 返回的 disposition 应为 Disposition.CANDIDATE_DELETE

Validates: Requirements 1.6

Property 5: tmp/ 下文件处置范围

对于任意 rel_pathtmp/ 开头的 FileEntryclassify 返回的 disposition 应为 Disposition.CANDIDATE_DELETEDisposition.CANDIDATE_ARCHIVE 之一。

Validates: Requirements 1.7

Property 6: 运行时产出目录标记为候选归档

对于任意 rel_pathlogs/export/ 开头且非 __init__.pyFileEntryclassify 返回的 disposition 应为 Disposition.CANDIDATE_ARCHIVE

Validates: Requirements 1.8

Property 7: 扫描器排除规则

对于任意 文件树,scan_repo 返回的 FileEntry 列表中不应包含 rel_path 匹配排除模式(.git__pycache__.pytest_cache)的条目。

Validates: Requirements 1.1

Property 8: 清单按分类分组

对于任意 InventoryItem 列表,render_inventory_report 生成的 Markdown 中,同一 Category 的条目应连续出现(即按分类分组排列)。

Validates: Requirements 1.10

Property 9: 流程树节点 source_file 有效性

对于任意 FlowNode 树中的节点,source_file 字段应为非空字符串,且对应的文件在仓库中实际存在。

Validates: Requirements 2.7

Property 10: 孤立模块检测正确性

对于任意 文件集合和可达模块集合,find_orphan_modules 返回的孤立模块列表中的每个模块都不应出现在可达集合中,且可达集合中的每个模块都不应出现在孤立列表中。

Validates: Requirements 2.8

Property 11: 过期引用检测

对于任意 文档中提取的代码引用,若该引用指向的文件路径在仓库中不存在,则 check_reference_validity 应返回 False

Validates: Requirements 3.3

Property 12: 缺失文档检测

对于任意 核心代码模块集合和已文档化模块集合,find_undocumented_modules 返回的缺失列表应恰好等于核心模块集合与已文档化集合的差集。

Validates: Requirements 3.5

Property 13: 统计摘要一致性

对于任意 报告的统计摘要,各分类/标签的计数之和应等于对应条目列表的总长度。

Validates: Requirements 4.5, 4.6, 4.7

Property 14: 报告头部元信息

对于任意 报告输出,头部应包含一个符合 ISO 格式的时间戳字符串和仓库根目录路径字符串。

Validates: Requirements 4.2

Property 15: 写操作仅限 docs/audit/

对于任意 审计执行过程,所有文件写操作的目标路径应以 docs/audit/ 为前缀。

Validates: Requirements 5.2

Property 16: 文档对齐报告分区完整性

对于任意 render_alignment_report 的输出Markdown 文本应包含"映射关系"、"过期点"、"冲突点"、"缺失点"四个分区标题。

Validates: Requirements 3.8

错误处理

场景 处理方式
文件读取权限不足 记录警告到报告的"错误"分区,跳过该文件,继续处理
Python 源文件语法错误(ast.parse 失败) 记录警告,将该文件标记为"待确认",不中断流程树构建
文档中的代码引用格式无法解析 跳过该引用,不产生误报
DDL 文件 SQL 语法不规范 使用正则提取 CREATE TABLE 和列定义,容忍非标准语法
docs/audit/ 目录创建失败 抛出异常并终止,因为无法输出报告
编码问题(非 UTF-8 文件) 尝试 utf-8gbklatin-1 回退读取,记录编码警告

测试策略

测试框架

  • 单元测试与属性测试均使用 pytest
  • 属性测试库:hypothesisPython 生态最成熟的属性测试框架)
  • 测试文件位于 tests/unit/test_audit_*.py

单元测试

针对具体示例和边界情况:

  • 扫描器对实际仓库子集的遍历结果
  • classify 对已知文件路径的分类正确性(如 tmp/hebing.py → 临时与调试/候选删除)
  • 入口点识别对实际仓库的结果
  • DDL 与数据字典的比对结果
  • 文件读取失败时的容错行为
  • docs/audit/ 目录不存在时的自动创建

属性测试

每个正确性属性对应一个属性测试,使用 hypothesis 生成随机输入:

  • 每个属性测试至少运行 100 次迭代
  • 每个测试用注释标注对应的设计属性编号
  • 标注格式:Feature: repo-audit, Property {N}: {属性标题}

生成器策略

  • FileEntry 生成器:随机路径(含各种扩展名、目录层级)、随机大小、随机 is_dir/is_empty_dir
  • InventoryItem 生成器:随机 Category/Disposition 组合、随机描述文本
  • FlowNode 生成器:随机树结构(限制深度和宽度)
  • 文件树生成器:构造临时目录结构用于扫描器测试