# 设计文档:仓库治理只读审计 ## 概述 本设计描述三个 Python 审计脚本的实现方案,用于对 etl-billiards 仓库进行只读分析并生成三份 Markdown 报告。脚本仅读取文件系统和源代码,不连接数据库、不修改任何现有文件,仅在 `docs/audit/` 目录下输出报告。 审计脚本采用模块化设计:一个共享的仓库扫描器负责遍历文件系统,三个独立的分析器分别生成文件清单、流程树和文档对齐报告。 ## 架构 ```mermaid graph TD A[scripts/audit/run_audit.py
审计主入口] --> B[scripts/audit/scanner.py
仓库扫描器] A --> C[scripts/audit/inventory_analyzer.py
文件清单分析器] A --> D[scripts/audit/flow_analyzer.py
流程树分析器] A --> E[scripts/audit/doc_alignment_analyzer.py
文档对齐分析器] B --> F[文件系统
只读遍历] 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`) 负责递归遍历仓库文件系统,返回结构化的文件元信息。 ```python @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`) 对扫描结果进行用途分类和处置标签分配。 ```python # 用途分类枚举 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` 语句和类继承关系,构建从入口到末端模块的调用树。 ```python @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`) 检查文档与代码之间的映射关系、过期点、冲突点和缺失点。 ```python @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`) ```python 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_dir - `InventoryItem` 生成器:随机 Category/Disposition 组合、随机描述文本 - `FlowNode` 生成器:随机树结构(限制深度和宽度) - 文件树生成器:构造临时目录结构用于扫描器测试