16 KiB
设计文档:仓库治理只读审计
概述
本设计描述三个 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
执行流程
run_audit.py作为主入口,初始化扫描器并依次调用三个分析器scanner.py递归遍历仓库,构建文件元信息列表(路径、大小、类型)- 各分析器接收扫描结果,执行各自的分析逻辑,输出 Markdown 报告
- 所有报告写入
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/*.sql、database/migrations/ |
数据库定义 | 保留 |
database/*.py |
核心代码 | 保留 |
tests/ |
测试 | 保留 |
docs/ |
文档 | 保留 |
scripts/ 下的 .py 文件 |
脚本工具 | 保留/待确认 |
gui/ |
GUI | 保留 |
setup.py、build_exe.py、*.bat、*.sh、*.ps1 |
构建与部署 | 保留 |
根目录散落文件(Prompt用.md、Untitled、fix_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.py→main()函数 - GUI 入口:
gui/main.py→main()函数 - 批处理入口:
run_etl.bat、run_gui.bat、run_ods.bat→ 解析其中的python命令 - 运维脚本:
scripts/*.py→ 各自的if __name__ == "__main__"块
静态分析策略:
- 使用 Python
ast模块解析源文件,提取import和from ... import语句 - 仅追踪项目内部模块(排除标准库和第三方包)
- 通过
orchestration/task_registry.py的注册语句识别所有任务类及其源文件 - 通过类继承关系(
BaseTask、BaseLoader、BaseDwsTask等)识别任务和加载器层级
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.md(gui/README.md、fetch-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 完整性
对于任意 FileEntry,classify 函数返回的 InventoryItem 的 category 字段应属于 Category 枚举,disposition 字段应属于 Disposition 枚举,且 description 字段为非空字符串。
Validates: Requirements 1.2, 1.3
Property 2: 清单渲染完整性
对于任意 InventoryItem 列表,render_inventory_report 生成的 Markdown 文本中,每个条目对应的行应包含该条目的 rel_path、category.value、disposition.value 和 description 四个字段。
Validates: Requirements 1.4
Property 3: 空目录标记为候选删除
对于任意 is_empty_dir=True 的 FileEntry,classify 返回的 disposition 应为 Disposition.CANDIDATE_DELETE。
Validates: Requirements 1.5
Property 4: .lnk/.rar 文件标记为候选删除
对于任意 扩展名为 .lnk 或 .rar 的 FileEntry,classify 返回的 disposition 应为 Disposition.CANDIDATE_DELETE。
Validates: Requirements 1.6
Property 5: tmp/ 下文件处置范围
对于任意 rel_path 以 tmp/ 开头的 FileEntry,classify 返回的 disposition 应为 Disposition.CANDIDATE_DELETE 或 Disposition.CANDIDATE_ARCHIVE 之一。
Validates: Requirements 1.7
Property 6: 运行时产出目录标记为候选归档
对于任意 rel_path 以 logs/ 或 export/ 开头且非 __init__.py 的 FileEntry,classify 返回的 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-8 → gbk → latin-1 回退读取,记录编码警告 |
测试策略
测试框架
- 单元测试与属性测试均使用
pytest - 属性测试库:
hypothesis(Python 生态最成熟的属性测试框架) - 测试文件位于
tests/unit/test_audit_*.py
单元测试
针对具体示例和边界情况:
- 扫描器对实际仓库子集的遍历结果
- classify 对已知文件路径的分类正确性(如
tmp/hebing.py→ 临时与调试/候选删除) - 入口点识别对实际仓库的结果
- DDL 与数据字典的比对结果
- 文件读取失败时的容错行为
docs/audit/目录不存在时的自动创建
属性测试
每个正确性属性对应一个属性测试,使用 hypothesis 生成随机输入:
- 每个属性测试至少运行 100 次迭代
- 每个测试用注释标注对应的设计属性编号
- 标注格式:Feature: repo-audit, Property {N}: {属性标题}
生成器策略:
FileEntry生成器:随机路径(含各种扩展名、目录层级)、随机大小、随机 is_dir/is_empty_dirInventoryItem生成器:随机 Category/Disposition 组合、随机描述文本FlowNode生成器:随机树结构(限制深度和宽度)- 文件树生成器:构造临时目录结构用于扫描器测试