Files
chorus/dev_scripts/harness.py
T

747 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""DevHarness 单一命令行入口。
子命令:
check 检查必需文件、核心文档和已有任务快照结构
sync 从 Gitea Wiki 单向导出或校验核心 docs 镜像
archive 显式在 Gitea Wiki 创建可选任务快照
export 人工按需把已有 Wiki 任务快照导出到 docs/task
各子命令的实现逻辑取自原来的 check_harness.py、sync_wiki_docs.py、
new_task_archive.py 和 export_task_archives.py,行为未改变。
"""
from __future__ import annotations
import argparse
import re
from datetime import date
from pathlib import Path
from typing import Any
from wiki_docs import (
DEFAULT_CONFIG,
WikiClient,
WikiDocsError,
dirty_paths,
load_config,
parse_mirror,
sync_all,
write_mirror,
)
# ---------------------------------------------------------------- 结构检查
ROOT = Path(__file__).resolve().parents[1]
CORE_PAGE_PATHS = {
"Home": "docs/README.md",
"Project-Profile": "docs/00-project-profile.md",
"Development-Workflow": "docs/01-workflow.md",
"Architecture-and-Code-Map": "docs/02-architecture-and-code-map.md",
"Business-Rules-and-Glossary": "docs/03-business-rules-and-glossary.md",
"Local-Development-and-Verification": (
"docs/04-local-development-and-verification.md"
),
"Common-Changes": "docs/05-common-changes.md",
"Troubleshooting": "docs/06-troubleshooting.md",
"New-Project-Documentation-Setup": (
"docs/07-new-project-documentation-setup.md"
),
"Existing-Project-Adoption-Guide": (
"docs/08-existing-project-adoption.md"
),
"Product-Requirements-Overview": (
"docs/09-product-requirements-overview.md"
),
"Delivery-Documentation-Guide": "docs/delivery/README.md",
"Audience-Document-Template": (
"docs/delivery/audience-document-template.md"
),
"Task-Archive-Template": "docs/templates/task-archive.md",
}
CORE_DOCUMENT_REQUIREMENTS = {
"docs/README.md": (
"## 第一次阅读",
"## 五分钟开始",
"## 简单修改从哪里开始",
"## 事实来源",
),
"docs/00-project-profile.md": (
"## 基本信息",
"## DevHarness 来源与基线",
"## 子项目与交付单元",
"## 技术栈与运行环境",
"## 阅读入口",
"## 常用命令",
"## 环境、配置与凭据",
),
"docs/01-workflow.md": (
"## Gitea 交互与工单最小读取",
"## 新项目 Wiki 初始化门禁",
"## 工单与设计证据双门禁",
"### 先判断是否需要工单",
"### 再判断设计证据",
"### 线上原型审核与按需导出",
"### 记录和重新确认",
"## 面向初级维护者的修改边界",
"## 每个任务的文档影响",
"## 需求记录与流转",
"## 稳定文档与可选历史快照",
"## 自然语言快捷指令",
"## 效率与范围控制",
"### 严格控制范围",
"### 渐进执行和修复",
"### 复用已验证事实",
"### 明确停止条件",
),
"docs/02-architecture-and-code-map.md": (
"## 项目定位",
"## 代码地图",
"## 两条主要执行路径",
"## 不可破坏的边界",
),
"docs/03-business-rules-and-glossary.md": (
"## 核心术语",
"## 工单状态",
"## 稳定业务规则",
"## 新项目需要补充什么",
),
"docs/04-local-development-and-verification.md": (
"## 环境要求",
"## 第一次运行",
"## 常用调试方式",
"## 完成修改前",
),
"docs/05-common-changes.md": (
"## 风险分级",
"## 修改 Wiki 文案",
"## 调整 Harness 检查",
"## 看懂 Agent 的修改",
),
"docs/06-troubleshooting.md": (
"## 排查顺序",
"## 必须停止的情况",
),
"docs/07-new-project-documentation-setup.md": (
"## 初始化顺序",
"#### 需求总览启用条件",
"### 2. 选择建设基线",
"#### 工程基线裁剪",
"#### 判断案例",
"### 3. 识别子项目与交付单元",
"### 7. 确定交付对象和文档",
"#### 在线创建与回读门禁",
"## 完成标准",
),
"docs/08-existing-project-adoption.md": (
"## 与新项目初始化的区别",
"## 接入前只读盘点",
"## 已有内容保护原则",
"## 多应用单仓库判断",
"### 适合继续单仓库",
"### 可以考虑拆仓",
"### 保持单仓库时的最小规则",
"## 增量接入顺序",
"## 后续升级",
"### 升级步骤",
"### 可复制升级指令",
"## 冲突处理和停止条件",
"## 可复制 Agent 指令",
"### 只分析",
"### 方案确认后实施",
"## 最小验收清单",
"## 回退原则",
),
"docs/09-product-requirements-overview.md": (
"## 本页用途",
"## 事实来源边界",
"## 当前需求索引",
"## 登记规则",
"## 原型与设计资产",
"### 原型门禁",
"### 线上原型与按需 HTML 快照",
"### 原型确认记录",
"## 状态规则",
"## 更新时机",
"## 最小验收清单",
),
"docs/delivery/README.md": (
"## 什么时候需要交付文档",
"## 受众与文档选择",
"## 内部文档与交付文档边界",
"## 编写和维护流程",
"## 最小验收清单",
),
"docs/delivery/audience-document-template.md": (
"## 文档信息",
"## 目的与适用范围",
"## 前置条件",
"## 操作步骤",
"## 常见错误与恢复",
"## 安全与权限",
"## 已知限制",
"## 支持与升级处理",
"## 版本记录",
"## 交付前检查",
),
}
REQUIRED_FILES = (
"AGENTS.md",
"CLAUDE.md",
"README.md",
"docs/00-project-profile.md",
"docs/01-workflow.md",
"docs/templates/deployment.md",
"docs/templates/task-archive.md",
*CORE_DOCUMENT_REQUIREMENTS,
"wiki-docs.json",
"dev_scripts/wiki_docs.py",
"dev_scripts/harness.py",
".gitea/issue_template/epic.md",
".gitea/issue_template/mvp.md",
".gitea/issue_template/task.md",
)
ARCHIVE_HEADINGS = (
"## 背景与目标",
"## 最终方案",
"## 修改文件",
"## 验收结果",
"## 测试",
"## 相关提交",
)
def check_required_files(errors: list[str]) -> None:
for relative_path in REQUIRED_FILES:
if not (ROOT / relative_path).is_file():
errors.append(f"缺少必需文件:{relative_path}")
def check_project_profile(errors: list[str], warnings: list[str], strict: bool) -> None:
profile = ROOT / "docs" / "00-project-profile.md"
if not profile.is_file():
return
if "<填写" in profile.read_text(encoding="utf-8"):
message = "项目档案仍有未填写内容"
(errors if strict else warnings).append(message)
def check_archives(errors: list[str]) -> None:
task_dir = ROOT / "docs" / "task"
for path in task_dir.glob("*.md"):
if not re.match(r"^\d+-.+\.md$", path.name):
errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}")
content = path.read_text(encoding="utf-8")
try:
metadata, _ = parse_mirror(content)
except WikiDocsError as exc:
errors.append(f"{path.name} 的任务镜像无效:{exc}")
continue
if re.fullmatch(r"Task-\d+-.+", metadata.get("wiki_page", "")) is None:
errors.append(f"{path.name} 的 wiki_page 不是任务归档页面")
if re.fullmatch(r"[0-9a-f]{40,64}", metadata.get("wiki_revision", "")) is None:
errors.append(f"{path.name} 的 wiki_revision 无效")
for heading in ARCHIVE_HEADINGS:
if heading not in content:
errors.append(f"{path.name} 缺少章节:{heading}")
if "**未验证部分**:" not in content:
errors.append(f"{path.name} 没有记录未验证部分")
def missing_sections(content: str, required: tuple[str, ...]) -> list[str]:
return [section for section in required if section not in content]
def check_core_documents(errors: list[str], root: Path = ROOT) -> None:
"""检查初级维护者所需主题页的固定结构。"""
for relative_path, required in CORE_DOCUMENT_REQUIREMENTS.items():
path = root / relative_path
if not path.is_file():
continue
try:
_, body = parse_mirror(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, WikiDocsError):
continue
for section in missing_sections(body, required):
errors.append(f"{relative_path} 缺少核心章节:{section}")
def check_task_template(errors: list[str], root: Path = ROOT) -> None:
path = root / ".gitea" / "issue_template" / "task.md"
if not path.is_file():
return
content = path.read_text(encoding="utf-8")
required = (
"## 依赖与并行",
"- 前置工单:无 / #编号",
"- 是否允许与前置工单并行:是 / 否",
"- 原因:",
"## 子项目影响",
"- 仅影响的子项目 / 交付单元:",
"- 是否跨子项目:是 / 否",
"- 是否修改共享接口或契约:是 / 否;唯一事实来源:",
"- 各子项目需要执行的验证:",
"## 原始需求",
"- 来源:用户对话 / Gitea / 其他",
"- 提出时间:",
"- 关键原话或脱敏摘要:",
"## 需求变化记录",
"| 日期 | 变化内容 | 原因 | 用户确认 |",
"## 设计与原型门禁",
"- 修改类型:纯显示文案 / 小范围 UI / 新组件 / 新页面或独立用户功能 / 重大交互或导航 / 非 UI / 恢复既有行为的 Bug",
"- 所需设计证据:无 / 标注截图 / 低保真图 / 已确认原型 / 架构、API、数据、状态或流程设计 / 原设计或复现证据",
"- 可编辑设计源、线上原型链接和访问检查:",
"- 审核版本、revision、复制版本或确认日期及识别方式:",
"- 本地 HTML 导出:未要求 / 用户明确要求 / 项目规则要求",
"- 本地 HTML 路径、版本和资源检查(仅显式导出时填写):",
"- 状态:无 / 草稿 / 已确认 / 已废弃",
"- 确认人、确认时间和覆盖范围:",
"- 无需 UI 原型或无需任何原型的原因:",
"## 文档影响",
"- [ ] 不影响长期文档,原因:",
"- [ ] 更新架构与代码地图",
"- [ ] 更新业务规则与术语",
"- [ ] 更新常见修改或故障排查",
"## 交付文档影响",
"- [ ] 无交付文档影响,原因:",
"- [ ] 更新已有交付文档,受众与页面:",
"- [ ] 新增交付文档,受众与页面:",
"- [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式:",
"## 任务记录与可选快照",
"- 单次任务事实来源:当前 Gitea 工单正文与评论",
"- [ ] 默认不创建任务快照",
"- [ ] 用户明确要求专项快照;用途和范围:",
"- [ ] 项目专用规则要求任务快照;规则入口:",
)
for section in missing_sections(content, required):
errors.append(f"单元任务模板缺少:{section}")
def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None:
path = root / "AGENTS.md"
if not path.is_file():
return
content = path.read_text(encoding="utf-8")
required = (
"### 效率与范围控制",
"#### 严格控制范围",
"#### 渐进执行和修复",
"#### 复用已验证事实",
"#### 明确停止条件",
"单元任务是唯一正式实施单位",
"高风险修改必须停止",
"用户没有明确验收通过前不得关闭",
"只有长期事实变化时才修改 Wiki",
"Gitea 工单是单次任务需求、变化、实现、测试、提交和验收的事实来源",
"默认不创建任务归档",
"### Gitea 交互与工单最小读取",
"查询、创建、更新、评论、状态变更及关闭操作",
"优先关注当前状态、最新评论和首个未完成步骤",
"连接器不支持评论分页或增量读取时允许读取完整工单",
"不得为规避完整读取而新增本地工单、缓存或第二事实来源",
"### 新项目 Wiki 初始化门禁",
"`Home` 不存在时必须先创建 `Home`",
"不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据",
"提交只包含当前工单相关文件",
"### 工单与设计证据双门禁",
"新页面、独立用户功能、重大交互或导航变化",
"`prototypes/<工单号>/<版本>/index.html`",
"默认直接通过 Quant-UX 或其他设计工具的线上链接审核",
"已确认的本地快照不得原位覆盖",
"`导出原型 #N`",
"`导出全部原型`",
"代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案",
"### 自然语言快捷指令",
"`只分析`",
"`建工单`",
"`执行工单 #N`",
"`建工单并做`",
"`继续工单 #N`",
"`检查工单 #N`",
"`同步文档`",
"`导出原型 #N`",
"`导出全部原型`",
"`导出任务归档`",
"`导出全部任务归档`",
"`#N 验收通过`",
"### 需求记录与流转",
"不得臆造用户原话",
"不复制完整聊天",
"Gitea 工单全文不导出到仓库",
)
for section in missing_sections(content, required):
errors.append(f"AGENTS.md 缺少:{section}")
def check_repository_readme(errors: list[str], root: Path = ROOT) -> None:
"""检查快速开始包含线上 Wiki 初始化顺序和产品编码门禁。"""
path = root / "README.md"
if not path.is_file():
return
content = path.read_text(encoding="utf-8")
required = (
"创建 Gitea 远端仓库并推送当前引导提交,启用工单和 Wiki",
"优先使用已配置的 Gitea MCP",
"`Home` 不存在时先创建并回读 `Home`",
"本地 `docs/` 的存在不能证明线上 Wiki 已初始化",
"python dev_scripts/harness.py sync --verify",
"Gitea 工单是单次任务唯一事实来源",
"默认不创建任务归档",
)
for section in missing_sections(content, required):
errors.append(f"README.md 缺少:{section}")
remote_index = content.find("创建 Gitea 远端仓库")
wiki_index = content.find("`Home` 不存在时先创建")
if remote_index < 0 or wiki_index < 0 or remote_index > wiki_index:
errors.append("README.md 必须先创建 Gitea 远端,再创建 Wiki Home")
def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None:
"""检查 Claude Code 入口直接复用共同 Agent 规则。"""
path = root / "CLAUDE.md"
if not path.is_file():
return
content = path.read_text(encoding="utf-8")
lines = {line.strip() for line in content.splitlines()}
if "@AGENTS.md" not in lines:
errors.append("CLAUDE.md 缺少独立的 @AGENTS.md 导入")
required = (
"共同规则事实来源",
"只记录 Claude Code 特有",
"只修改 `AGENTS.md`",
"## 模型路由",
"## Agent 交接",
"## Haiku 只读约束",
"当前模型足以完成任务时不升级模型",
"Opus 输出方案后必须等待用户确认",
"不让 Haiku 决定最终根因",
"只读必须通过子 Agent 工具权限实现",
)
for section in missing_sections(content, required):
errors.append(f"CLAUDE.md 缺少:{section}")
def core_mapping_errors(configured_mappings: dict[str, str]) -> list[str]:
errors: list[str] = []
for page, expected_path in CORE_PAGE_PATHS.items():
if configured_mappings.get(page) != expected_path:
errors.append(
f"核心 Wiki 页面映射缺失或路径错误:{page} -> {expected_path}"
)
return errors
def check_wiki_mirrors(errors: list[str]) -> None:
"""检查核心映射与镜像头;任务快照由 check_archives 单独检查。"""
try:
config = load_config()
except WikiDocsError as exc:
errors.append(str(exc))
return
configured_mappings = {mapping.page: mapping.path for mapping in config.mappings}
errors.extend(core_mapping_errors(configured_mappings))
mapped_paths = {mapping.path for mapping in config.mappings}
actual_paths = {
path.relative_to(ROOT).as_posix() for path in (ROOT / "docs").rglob("*.md")
if path.parent != ROOT / "docs" / "task"
}
for path in sorted(actual_paths - mapped_paths):
errors.append(f"docs 中存在未登记的 Wiki 镜像:{path}")
for mapping in config.mappings:
path = ROOT / mapping.path
if not path.is_file():
errors.append(f"缺少 Wiki 镜像:{mapping.path}")
continue
try:
metadata, _ = parse_mirror(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, WikiDocsError) as exc:
errors.append(f"Wiki 镜像无效 {mapping.path}:{exc}")
continue
if metadata.get("generated") != "true (请先修改 Gitea Wiki,禁止直接编辑本文件)":
errors.append(f"{mapping.path} 没有只读镜像标记")
if metadata.get("wiki_page") != mapping.page:
errors.append(f"{mapping.path} 的 wiki_page 与映射不一致")
revision = metadata.get("wiki_revision", "")
if re.fullmatch(r"[0-9a-f]{40,64}", revision) is None:
errors.append(f"{mapping.path} 的 wiki_revision 无效")
if not metadata.get("synchronized_at"):
errors.append(f"{mapping.path} 缺少 synchronized_at")
# ---------------------------------------------------------------- 任务归档
def safe_title(title: str) -> str:
"""把标题转换为适合 Wiki 页面名和 Windows 文件名的短文本。"""
cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip())
cleaned = re.sub(r"\s+", "-", cleaned)
cleaned = re.sub(r"-+", "-", cleaned)
return cleaned.strip(".-")
def build_archive(
template: str,
issue_number: str,
title: str,
page_name: str,
issue_url: str,
) -> str:
content = template.replace("<工单号>", issue_number, 1)
content = content.replace("<标题>", title.strip(), 1)
content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1)
content = content.replace("<链接>", issue_url, 1)
return content.replace("<页面名>", page_name, 1)
# ---------------------------------------------------------------- 归档导出
TASK_PAGE_PATTERN = re.compile(r"^Task-(?P<number>\d+)-(?P<title>.+)$")
def task_revision(metadata: dict[str, Any], page_name: str) -> str:
last_commit = metadata.get("last_commit")
revision = last_commit.get("sha") if isinstance(last_commit, dict) else None
if not isinstance(revision, str) or not revision:
raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}")
return revision
def existing_task_mirrors(root: Path = ROOT) -> dict[str, Path]:
"""按镜像头匹配已有文件,兼容历史自定义文件名。"""
mirrors: dict[str, Path] = {}
task_dir = root / "docs" / "task"
if not task_dir.is_dir():
return mirrors
for path in task_dir.glob("*.md"):
try:
metadata, _ = parse_mirror(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, WikiDocsError) as exc:
raise WikiDocsError(f"已有任务镜像无效 {path.name}:{exc}") from exc
page_name = metadata.get("wiki_page", "")
if not TASK_PAGE_PATTERN.fullmatch(page_name):
raise WikiDocsError(f"已有任务镜像页面名无效 {path.name}:{page_name}")
if page_name in mirrors:
raise WikiDocsError(f"任务页面存在重复本地镜像:{page_name}")
mirrors[page_name] = path
return mirrors
def task_target(page_name: str, root: Path = ROOT) -> Path:
match = TASK_PAGE_PATTERN.fullmatch(page_name)
if match is None:
raise WikiDocsError(f"不是任务归档页面:{page_name}")
title = safe_title(match.group("title"))
if not title:
raise WikiDocsError(f"任务归档标题无效:{page_name}")
return root / "docs" / "task" / f"{match.group('number')}-{title}.md"
def export_task_archives(
client: WikiClient, *, export_all: bool = False, root: Path = ROOT
) -> list[str]:
"""增量或全量读取任务归档;绝不删除本地文件。"""
dirty = dirty_paths(["docs/task"], root)
if dirty:
raise WikiDocsError(
"本地任务镜像存在未提交改动,已停止以防覆盖:\n" + "\n".join(dirty)
)
existing = existing_task_mirrors(root)
pages = []
for metadata in client.list_pages():
title = metadata.get("title")
if isinstance(title, str) and TASK_PAGE_PATTERN.fullmatch(title):
pages.append((int(title.split("-", 2)[1]), title, metadata))
pages.sort(key=lambda item: (item[0], item[1]))
messages: list[str] = []
targets: set[Path] = set()
for _, page_name, metadata in pages:
target = existing.get(page_name, task_target(page_name, root))
if target in targets:
raise WikiDocsError(f"多个任务页面映射到同一本地路径:{target.name}")
targets.add(target)
revision = task_revision(metadata, page_name)
if not export_all and target.is_file():
local_metadata, _ = parse_mirror(target.read_text(encoding="utf-8"))
if (
local_metadata.get("wiki_page") == page_name
and local_metadata.get("wiki_revision") == revision
):
messages.append(f"跳过:{target.relative_to(root)} <- {page_name}@{revision[:12]}")
continue
page = client.get_page_from_metadata(metadata, page_name)
changed = write_mirror(target, page)
action = "已导出" if changed else "无变化"
messages.append(f"{action}:{target.relative_to(root)} <- {page_name}@{revision[:12]}")
return messages
# ---------------------------------------------------------------- 子命令入口
def run_check(args: argparse.Namespace) -> int:
errors: list[str] = []
warnings: list[str] = []
check_required_files(errors)
check_project_profile(errors, warnings, args.strict)
check_wiki_mirrors(errors)
check_core_documents(errors)
check_task_template(errors)
check_agent_efficiency_rules(errors)
check_repository_readme(errors)
check_claude_code_entry(errors)
check_archives(errors)
for warning in warnings:
print(f"警告:{warning}")
for error in errors:
print(f"错误:{error}")
if errors:
print(f"检查失败:{len(errors)} 个问题")
return 1
print("DevHarness 检查通过")
return 0
def run_sync(args: argparse.Namespace) -> int:
"""--verify 依次执行导出、结构检查和一致性校验,替代原来的三条命令。"""
if args.verify:
steps = (
("同步", lambda: run_sync(
argparse.Namespace(check=False, verify=False, config=args.config))),
("结构检查", lambda: run_check(argparse.Namespace(strict=True))),
("一致性校验", lambda: run_sync(
argparse.Namespace(check=True, verify=False, config=args.config))),
)
for name, step in steps:
code = step()
if code != 0:
print(f"错误:{name}未通过,已停止")
return code
return 0
try:
config = load_config(Path(args.config).resolve())
messages = sync_all(config, WikiClient(config), check=args.check)
except WikiDocsError as exc:
print(f"错误:{exc}")
return 1
for message in messages:
print(message)
print("Wiki 镜像检查通过" if args.check else "Wiki 镜像同步完成")
return 0
def run_archive(args: argparse.Namespace) -> int:
short_title = safe_title(args.title)
if not args.issue_number.isdigit():
print("错误:工单号必须是数字")
return 1
if not short_title:
print("错误:标题不能为空")
return 1
try:
config = load_config(Path(args.config).resolve())
page_name = f"Task-{args.issue_number}-{short_title}"
client = WikiClient(config)
if any(item.get("title") == page_name for item in client.list_pages()):
raise WikiDocsError(f"任务归档已经存在:{page_name}")
template = client.get_page("Task-Archive-Template").text
issue_url = (
f"{config.gitea_url}/{config.owner}/{config.repository}/issues/"
f"{args.issue_number}"
)
content = build_archive(
template, args.issue_number, args.title, page_name, issue_url
)
page = client.create_page(
page_name,
content,
f"docs: 创建任务 #{args.issue_number} 归档草稿",
)
except WikiDocsError as exc:
print(f"错误:{exc}")
return 1
print(f"已创建 Wiki:{page.html_url}")
print("未导出本地任务归档;需要时运行 harness.py export")
return 0
def run_export(args: argparse.Namespace) -> int:
try:
config = load_config(Path(args.config).resolve())
messages = export_task_archives(WikiClient(config), export_all=args.all)
except WikiDocsError as exc:
print(f"错误:{exc}")
return 1
for message in messages:
print(message)
print("任务归档全量导出完成" if args.all else "任务归档增量导出完成")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="DevHarness 检查、同步与归档工具")
sub = parser.add_subparsers(dest="command", required=True)
p_check = sub.add_parser("check", help="检查 DevHarness 项目结构")
p_check.add_argument(
"--strict", action="store_true", help="项目档案有占位内容时返回失败"
)
p_check.set_defaults(func=run_check)
p_sync = sub.add_parser("sync", help="从 Gitea Wiki 单向同步核心 docs 镜像")
p_sync.add_argument(
"--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件"
)
p_sync.add_argument(
"--verify",
action="store_true",
help="依次执行导出、check --strict 和一致性校验",
)
p_sync.add_argument(
"--config", default=str(DEFAULT_CONFIG), help="Wiki 页面映射 JSON 文件"
)
p_sync.set_defaults(func=run_sync)
p_archive = sub.add_parser("archive", help="显式在 Gitea Wiki 创建可选任务快照")
p_archive.add_argument("issue_number", help="Gitea 工单号,例如 123")
p_archive.add_argument("title", help="简短任务标题")
p_archive.add_argument(
"--config", default=str(DEFAULT_CONFIG), help="Wiki 映射配置"
)
p_archive.set_defaults(func=run_archive)
p_export = sub.add_parser("export", help="人工按需导出 Gitea Wiki 任务归档")
p_export.add_argument(
"--all",
action="store_true",
help="全量读取全部线上任务归档;默认按 revision 增量",
)
p_export.add_argument(
"--config", default=str(DEFAULT_CONFIG), help="核心 Wiki 映射配置"
)
p_export.set_defaults(func=run_export)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())