@hupan56/wlkj 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/bin/cli.js +213 -0
  2. package/package.json +11 -0
  3. package/templates/cli.js +198 -0
  4. package/templates/qoder/commands/wl-code.md +43 -0
  5. package/templates/qoder/commands/wl-commit.md +30 -0
  6. package/templates/qoder/commands/wl-init.md +80 -0
  7. package/templates/qoder/commands/wl-insight.md +51 -0
  8. package/templates/qoder/commands/wl-prd.md +199 -0
  9. package/templates/qoder/commands/wl-report.md +166 -0
  10. package/templates/qoder/commands/wl-search.md +52 -0
  11. package/templates/qoder/commands/wl-spec.md +18 -0
  12. package/templates/qoder/commands/wl-status.md +51 -0
  13. package/templates/qoder/commands/wl-task.md +71 -0
  14. package/templates/qoder/commands/wl-test.md +42 -0
  15. package/templates/qoder/config.toml +5 -0
  16. package/templates/qoder/config.yaml +141 -0
  17. package/templates/qoder/hooks/inject-workflow-state.py +117 -0
  18. package/templates/qoder/hooks/session-start.py +204 -0
  19. package/templates/qoder/rules/wl-pipeline.md +105 -0
  20. package/templates/qoder/scripts/add_session.py +245 -0
  21. package/templates/qoder/scripts/benchmark.py +209 -0
  22. package/templates/qoder/scripts/build_style_index.py +268 -0
  23. package/templates/qoder/scripts/code_index.py +41 -0
  24. package/templates/qoder/scripts/collect_prds.py +31 -0
  25. package/templates/qoder/scripts/common/__init__.py +0 -0
  26. package/templates/qoder/scripts/common/active_task.py +230 -0
  27. package/templates/qoder/scripts/common/atomicio.py +172 -0
  28. package/templates/qoder/scripts/common/developer.py +161 -0
  29. package/templates/qoder/scripts/common/eval_api.py +144 -0
  30. package/templates/qoder/scripts/common/feishu.py +278 -0
  31. package/templates/qoder/scripts/common/filelock.py +211 -0
  32. package/templates/qoder/scripts/common/identity.py +285 -0
  33. package/templates/qoder/scripts/common/mentions.py +134 -0
  34. package/templates/qoder/scripts/common/paths.py +311 -0
  35. package/templates/qoder/scripts/common/reqid.py +218 -0
  36. package/templates/qoder/scripts/common/search_engine.py +205 -0
  37. package/templates/qoder/scripts/common/task_utils.py +342 -0
  38. package/templates/qoder/scripts/common/terms.py +234 -0
  39. package/templates/qoder/scripts/common/utf8.py +38 -0
  40. package/templates/qoder/scripts/context_pack.py +196 -0
  41. package/templates/qoder/scripts/eval_prd.py +225 -0
  42. package/templates/qoder/scripts/export.py +487 -0
  43. package/templates/qoder/scripts/git_sync.py +1087 -0
  44. package/templates/qoder/scripts/handoff.py +22 -0
  45. package/templates/qoder/scripts/init_developer.py +76 -0
  46. package/templates/qoder/scripts/init_doctor.py +527 -0
  47. package/templates/qoder/scripts/install_qoderwork.py +339 -0
  48. package/templates/qoder/scripts/learn.py +67 -0
  49. package/templates/qoder/scripts/notify.py +5 -0
  50. package/templates/qoder/scripts/parse_prds.py +33 -0
  51. package/templates/qoder/scripts/report.py +281 -0
  52. package/templates/qoder/scripts/role.py +39 -0
  53. package/templates/qoder/scripts/run_weekly_update.bat +17 -0
  54. package/templates/qoder/scripts/run_weekly_update.sh +20 -0
  55. package/templates/qoder/scripts/search_index.py +352 -0
  56. package/templates/qoder/scripts/setup.py +453 -0
  57. package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
  58. package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
  59. package/templates/qoder/scripts/status.py +389 -0
  60. package/templates/qoder/scripts/syncgate.py +330 -0
  61. package/templates/qoder/scripts/task.py +954 -0
  62. package/templates/qoder/scripts/team.py +29 -0
  63. package/templates/qoder/scripts/team_sync.py +419 -0
  64. package/templates/qoder/scripts/workspace_init.py +102 -0
  65. package/templates/qoder/settings.json +53 -0
  66. package/templates/qoder/skills/design-review/SKILL.md +25 -0
  67. package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
  68. package/templates/qoder/skills/prd-review/SKILL.md +36 -0
  69. package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
  70. package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
  71. package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
  72. package/templates/qoder/skills/test-generator/SKILL.md +72 -0
  73. package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
  74. package/templates/qoder/skills/wl-init/SKILL.md +67 -0
  75. package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
  76. package/templates/qoder/skills/wl-report/SKILL.md +87 -0
  77. package/templates/qoder/skills/wl-search/SKILL.md +75 -0
  78. package/templates/qoder/skills/wl-status/SKILL.md +61 -0
  79. package/templates/qoder/skills/wl-task/SKILL.md +58 -0
  80. package/templates/qoder/templates/prd-full-template.md +103 -0
  81. package/templates/qoder/templates/prd-quick-template.md +69 -0
  82. package/templates/qoder/templates/prototype-app.html +344 -0
  83. package/templates/qoder/templates/prototype-web.html +310 -0
  84. package/templates/root/AGENTS.md +182 -0
  85. package/templates/root/README-pipeline.md +56 -0
  86. package/templates/root/ROLES.md +85 -0
  87. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
@@ -0,0 +1,172 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ atomicio.py - 原子读写工具
4
+
5
+ 解决"写一半崩溃损坏文件"和"读损坏 JSON 静默吞错"两类问题。
6
+
7
+ 核心 API:
8
+ atomic_write_text(path, content) # temp + os.replace, 写失败不影响原文件
9
+ atomic_write_json(path, data, ...) # 同上, JSON 序列化 + sorted keys
10
+ safe_read_json(path, ...) # 区分"缺失"vs"损坏"; 损坏备份 .corrupt 并抛错
11
+ atomic_append_jsonl(path, record) # 加锁的单行追加, 解决并发 JSONL 损坏
12
+
13
+ 为什么这样写:
14
+ - os.replace 在 Windows/POSIX 都是原子系统调用 (rename), 要么成功要么原文件不动
15
+ - JSON 损坏必须显式报错 + 备份, 而不是返回 {} 让上层以为"无数据"
16
+ - JSONL 并发追加: 同一进程内用 FileLock 串行化; 跨进程也靠 FileLock
17
+ """
18
+
19
+ import io
20
+ import json
21
+ import os
22
+ import sys
23
+ from datetime import datetime
24
+ from pathlib import Path
25
+ from typing import Any, Optional, Union
26
+
27
+ # 相对导入本模块所在 common/ 的兄弟 filelock
28
+ try:
29
+ from .filelock import FileLock
30
+ except ImportError: # 被直接当作脚本运行时
31
+ _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
32
+ if _COMMON_DIR not in sys.path:
33
+ sys.path.insert(0, _COMMON_DIR)
34
+ from filelock import FileLock
35
+
36
+ __all__ = [
37
+ "atomic_write_text",
38
+ "atomic_write_json",
39
+ "safe_read_json",
40
+ "atomic_append_jsonl",
41
+ "AtomicIOError",
42
+ ]
43
+
44
+ _PathLike = Union[str, "os.PathLike[str]"]
45
+
46
+
47
+ class AtomicIOError(Exception):
48
+ """原子 IO 失败的统一异常。"""
49
+
50
+
51
+ def atomic_write_text(path: _PathLike, content: str, encoding: str = "utf-8") -> None:
52
+ """原子写文本。先写 .tmp 再 os.replace, 写失败原文件不变。"""
53
+ path = str(path)
54
+ parent = os.path.dirname(path) or "."
55
+ os.makedirs(parent, exist_ok=True)
56
+ tmp = path + ".tmp.%d" % os.getpid()
57
+ try:
58
+ # newline='' 防止 Windows 自动 \n->\r\n 转换跨平台读不回来
59
+ with io.open(tmp, "w", encoding=encoding, newline="") as f:
60
+ f.write(content)
61
+ f.flush()
62
+ try:
63
+ os.fsync(f.fileno())
64
+ except (OSError, AttributeError):
65
+ pass # 某些文件系统/管道不支持 fsync
66
+ os.replace(tmp, path)
67
+ except Exception:
68
+ # 清理残留 tmp, 不要污染目录
69
+ try:
70
+ if os.path.exists(tmp):
71
+ os.remove(tmp)
72
+ except OSError:
73
+ pass
74
+ raise
75
+
76
+
77
+ def atomic_write_json(
78
+ path: _PathLike,
79
+ data: Any,
80
+ indent: int = 2,
81
+ ensure_ascii: bool = False,
82
+ sort_keys: bool = True,
83
+ ) -> None:
84
+ """原子写 JSON。sorted keys 让 diff 稳定, ensure_ascii=False 保留中文可读。"""
85
+ text = json.dumps(
86
+ data, indent=indent, ensure_ascii=ensure_ascii, sort_keys=sort_keys
87
+ )
88
+ atomic_write_text(path, text)
89
+
90
+
91
+ def safe_read_json(
92
+ path: _PathLike,
93
+ default: Any = None,
94
+ required: bool = False,
95
+ backup_corrupt: bool = True,
96
+ ) -> Any:
97
+ """安全读 JSON。
98
+
99
+ Args:
100
+ path: 文件路径。
101
+ default: 文件缺失时返回的值 (required=False 时)。
102
+ required: True 时文件缺失也抛 FileNotFoundError。
103
+ backup_corrupt: True 时若 JSON 损坏, 备份为 <path>.corrupt.<ts> 再抛错。
104
+
105
+ Returns:
106
+ 解析后的对象; 缺失且非 required 返回 default。
107
+
108
+ Raises:
109
+ FileNotFoundError: required=True 且文件不存在。
110
+ AtomicIOError: JSON 解析失败 (损坏)。已备份为 .corrupt.<ts>。
111
+ """
112
+ path = str(path)
113
+ if not os.path.isfile(path):
114
+ if required:
115
+ raise FileNotFoundError(path)
116
+ return default
117
+ try:
118
+ with io.open(path, "r", encoding="utf-8") as f:
119
+ return json.load(f)
120
+ except (ValueError, json.JSONDecodeError) as e:
121
+ # 损坏 —— 备份后抛错, 绝不静默吞
122
+ if backup_corrupt:
123
+ ts = datetime.now().strftime("%Y%m%d-%H%M%S")
124
+ corrupt_path = "%s.corrupt.%s" % (path, ts)
125
+ try:
126
+ os.replace(path, corrupt_path)
127
+ except OSError:
128
+ corrupt_path = path # replace 失败就保留原位置
129
+ raise AtomicIOError(
130
+ "JSON 损坏: %s\n 错误: %s\n 已备份为 .corrupt.* (若启用)" % (path, e)
131
+ )
132
+
133
+
134
+ def atomic_append_jsonl(
135
+ path: _PathLike,
136
+ record: Any,
137
+ timeout: float = 10.0,
138
+ ) -> None:
139
+ """加锁追加一行 JSON 到 .jsonl 文件。
140
+
141
+ 解决多个进程/会话同时 append 导致的行交错损坏 (eval_prd 并发场景)。
142
+ 用 FileLock 串行化追加操作。
143
+
144
+ Args:
145
+ path: .jsonl 文件路径。
146
+ record: 任意可 JSON 序列化的对象 (通常是个 dict)。
147
+ timeout: 获取锁超时秒数。
148
+
149
+ Raises:
150
+ AtomicIOError: 序列化失败。
151
+ (来自 FileLock 的 LockTimeoutError): 锁竞争超时。
152
+ """
153
+ path = str(path)
154
+ parent = os.path.dirname(path) or "."
155
+ os.makedirs(parent, exist_ok=True)
156
+ try:
157
+ line = json.dumps(record, ensure_ascii=False, sort_keys=True)
158
+ except (TypeError, ValueError) as e:
159
+ raise AtomicIOError("无法序列化记录: %s" % e)
160
+
161
+ lock_path = path + ".lock"
162
+ with FileLock(lock_path, timeout=timeout):
163
+ # O_APPEND 模式: 操作系统保证每次 write 追加到文件末尾, 不覆盖
164
+ # 单次 write(< 整行) 在 POSIX 是原子的 (< PIPE_BUF), 在 Windows 也安全
165
+ # 因为有 FileLock 串行化
166
+ with io.open(path, "a", encoding="utf-8") as f:
167
+ f.write(line + "\n")
168
+ f.flush()
169
+ try:
170
+ os.fsync(f.fileno())
171
+ except (OSError, AttributeError):
172
+ pass
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ QODER Pipeline - 开发者管理工具
5
+
6
+ 提供开发者身份的初始化和管理功能:
7
+ - init_developer: 初始化开发者身份, 创建工作空间和日志
8
+ - ensure_developer: 确保开发者已初始化 (未初始化则退出)
9
+ - show_developer_info: 显示开发者信息
10
+
11
+ 开发者身份存储在 .qoder/.developer 文件中 (gitignored)
12
+ 工作空间在 workspace/members/<developer>/ 目录下 (与 workspace_init.py 一致)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+ # 将 scripts 目录加入路径以便导入 common 模块
22
+ import os
23
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
24
+
25
+ from common.paths import (
26
+ DIR_WORKFLOW,
27
+ FILE_DEVELOPER,
28
+ MEMBERS_DIR,
29
+ get_repo_root,
30
+ get_developer,
31
+ check_developer,
32
+ )
33
+
34
+
35
+ # =============================================================================
36
+ # 开发者初始化
37
+ # =============================================================================
38
+
39
+ def write_developer_file(name: str, role: str | None = None,
40
+ repo_root: Path | None = None) -> bool:
41
+ """写入 .qoder/.developer (规范格式: name=<name>)。"""
42
+ if repo_root is None:
43
+ repo_root = get_repo_root()
44
+ dev_file = repo_root / DIR_WORKFLOW / FILE_DEVELOPER
45
+ lines = [f"name={name}"]
46
+ if role:
47
+ lines.append(f"role={role}")
48
+ lines.append(f"initialized_at={datetime.now().isoformat()}")
49
+ try:
50
+ dev_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
51
+ return True
52
+ except (OSError, IOError) as e:
53
+ print(f"Error: Failed to create .developer file: {e}", file=sys.stderr)
54
+ return False
55
+
56
+
57
+ def init_developer(name: str, role: str | None = None,
58
+ repo_root: Path | None = None) -> bool:
59
+ """初始化开发者身份。
60
+
61
+ 创建以下内容:
62
+ - .qoder/.developer 文件 (name= / role= / initialized_at=)
63
+ - workspace/members/<name>/{journal,drafts,inbox}/ 目录
64
+ - 初始日志文件 journal/journal-1.md
65
+
66
+ Returns:
67
+ 成功返回 True, 失败返回 False。
68
+ """
69
+ if not name:
70
+ print("Error: developer name is required", file=sys.stderr)
71
+ return False
72
+
73
+ # 校验名称合法性 (允许字母数字、中文和部分符号)
74
+ if not all(c.isalnum() or c in "-_" for c in name):
75
+ print(f"Error: invalid developer name '{name}' (only alphanumeric, -, _ allowed)", file=sys.stderr)
76
+ return False
77
+
78
+ if repo_root is None:
79
+ repo_root = get_repo_root()
80
+
81
+ # 零信任: 切换身份时清除旧开发者的 current-task 指针
82
+ # (防止 B 在 A 用过的机器上看到 A 的活跃任务, 误操作)
83
+ try:
84
+ from .paths import get_developer, set_current_task
85
+ old_dev = get_developer(repo_root)
86
+ if old_dev and old_dev != name:
87
+ set_current_task(None, repo_root, developer=old_dev)
88
+ print(f"Note: 切换开发者 {old_dev} -> {name}, 已清除 {old_dev} 的活跃任务指针")
89
+ except Exception:
90
+ pass # 清理失败不阻塞初始化
91
+
92
+ # 1. 创建 .developer 文件
93
+ if not write_developer_file(name, role, repo_root):
94
+ return False
95
+
96
+ # 2. 创建个人工作空间 workspace/members/<name>/
97
+ personal = repo_root / "workspace" / "members" / name
98
+ try:
99
+ for sub in ("journal", "drafts", "inbox"):
100
+ (personal / sub).mkdir(parents=True, exist_ok=True)
101
+ except (OSError, IOError) as e:
102
+ print(f"Error: Failed to create workspace directory: {e}", file=sys.stderr)
103
+ return False
104
+
105
+ # 3. 创建初始日志文件
106
+ journal_file = personal / "journal" / "journal-1.md"
107
+ if not journal_file.exists():
108
+ today = datetime.now().strftime("%Y-%m-%d")
109
+ try:
110
+ journal_file.write_text(
111
+ f"# Journal - {name}\n\n> Started: {today}\n\n---\n\n",
112
+ encoding="utf-8"
113
+ )
114
+ except (OSError, IOError) as e:
115
+ print(f"Error: Failed to create journal file: {e}", file=sys.stderr)
116
+ return False
117
+
118
+ print(f"Developer initialized: {name}" + (f" ({role})" if role else ""))
119
+ print(f" Workspace: workspace/members/{name}/")
120
+ print(f" Journal: workspace/members/{name}/journal/journal-1.md")
121
+ return True
122
+
123
+
124
+ # =============================================================================
125
+ # 开发者校验
126
+ # =============================================================================
127
+
128
+ def ensure_developer(repo_root: Path | None = None) -> str:
129
+ """确保开发者已初始化, 未初始化则打印提示并退出。"""
130
+ developer = get_developer(repo_root)
131
+ if not developer:
132
+ print("Error: Developer not initialized.", file=sys.stderr)
133
+ print("Run: python .qoder/scripts/workspace_init.py <your-name>", file=sys.stderr)
134
+ sys.exit(1)
135
+ return developer
136
+
137
+
138
+ def show_developer_info(repo_root: Path | None = None) -> None:
139
+ """显示当前开发者信息。"""
140
+ from common.paths import get_developer_info, get_workspace_dir, get_active_journal_file
141
+
142
+ info = get_developer_info(repo_root)
143
+ if not info:
144
+ print("Developer: not initialized")
145
+ print("Run: python .qoder/scripts/workspace_init.py <your-name>")
146
+ return
147
+
148
+ print(f"Developer: {info.get('name', 'unknown')}")
149
+ if info.get('role'):
150
+ print(f"Role: {info['role']}")
151
+ print(f"Initialized: {info.get('initialized_at', 'unknown')}")
152
+
153
+ workspace = get_workspace_dir(repo_root)
154
+ if workspace:
155
+ print(f"Workspace: {workspace}")
156
+
157
+ journal = get_active_journal_file(repo_root)
158
+ if journal:
159
+ from common.paths import count_lines
160
+ lines = count_lines(journal)
161
+ print(f"Active Journal: {journal.name} ({lines} lines)")
@@ -0,0 +1,144 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ eval_api.py - EVA PRD 评估的程序化 API
4
+
5
+ 从 eval_prd.py 抽取的可复用入口, 供 team_sync 门禁和其他脚本调用。
6
+ eval_prd.py 的 main() 也改为调用本模块。
7
+
8
+ 核心 API:
9
+ evaluate(prd_path, html_path=None) -> dict
10
+
11
+ 返回 dict 结构:
12
+ {
13
+ "passed": bool, # 总分 >= 80%
14
+ "pct": float, # 百分比 0-100
15
+ "total": int, # 原始总分
16
+ "full": int, # 满分 (70 若无原型, 100 若有)
17
+ "a1": int, # 现实锚定分 (满分 40)
18
+ "a2": int | None, # 风格保真分 (满分 30, 无原型则 None)
19
+ "a3": int, # 模板完整分 (满分 30)
20
+ "r1": float, "r2": float | None, "r3": float, # 原始比率 0-1
21
+ "msg1": str, "msg2": str | None, "msg3": str,
22
+ "misses": list[str], # 主要问题汇总 (给门禁显示用)
23
+ "prd": str, # 文件名
24
+ }
25
+ """
26
+
27
+ import os
28
+ import sys
29
+
30
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
31
+ _SCRIPTS_DIR = os.path.dirname(_THIS_DIR)
32
+ if _SCRIPTS_DIR not in sys.path:
33
+ sys.path.insert(0, _SCRIPTS_DIR)
34
+
35
+ # 复用 eval_prd.py 的评分函数 (不重复实现)
36
+ from eval_prd import (
37
+ score_reality, score_style, score_template, read_text,
38
+ FULL_SECTIONS, QUICK_SECTIONS,
39
+ )
40
+
41
+ PASS_THRESHOLD = 80.0 # 百分比
42
+
43
+
44
+ def evaluate(prd_path, html_path=None):
45
+ """评估 PRD (和可选原型), 返回结构化结果 dict。
46
+
47
+ Args:
48
+ prd_path: PRD markdown 文件路径。
49
+ html_path: 可选的原型 HTML 路径。不传则 A2 跳过, 满分变 70。
50
+
51
+ Returns:
52
+ 上述结构的 dict。不抛异常 (评估本身的错误记入 misses)。
53
+
54
+ Raises:
55
+ FileNotFoundError: PRD 文件不存在。
56
+ """
57
+ if not os.path.isfile(prd_path):
58
+ raise FileNotFoundError("PRD 不存在: %s" % prd_path)
59
+
60
+ prd_text = read_text(prd_path)
61
+
62
+ # A1 现实锚定
63
+ try:
64
+ r1, msg1, miss1 = score_reality(prd_text)
65
+ except Exception as e:
66
+ r1, msg1, miss1 = 0.0, "评估异常: %s" % e, []
67
+ a1 = round(r1 * 40)
68
+
69
+ # A2 风格保真 (可选)
70
+ a2 = None
71
+ r2 = None
72
+ msg2 = None
73
+ miss2 = []
74
+ if html_path and os.path.isfile(html_path):
75
+ try:
76
+ r2, msg2, miss2 = score_style(read_text(html_path))
77
+ a2 = round(r2 * 30)
78
+ except Exception as e:
79
+ r2, msg2, miss2 = 0.0, "评估异常: %s" % e, []
80
+ a2 = 0
81
+
82
+ # A3 模板完整
83
+ try:
84
+ r3, msg3, miss3 = score_template(prd_text)
85
+ except Exception as e:
86
+ r3, msg3, miss3 = 0.0, "评估异常: %s" % e, []
87
+ a3 = round(r3 * 30)
88
+
89
+ total = a1 + (a2 if a2 is not None else 0) + a3
90
+ full = 100 if r2 is not None else 70
91
+ pct = (total / full * 100) if full else 0.0
92
+ passed = pct >= PASS_THRESHOLD
93
+
94
+ # 汇总主要问题 (给门禁一行式显示)
95
+ misses = []
96
+ if miss1:
97
+ misses.append("A1 字段/API 找不到: " + ", ".join(miss1[:5]))
98
+ if miss2:
99
+ misses.append("A2 非真源颜色/emoji: " + ", ".join(miss2[:5]))
100
+ if miss3:
101
+ misses.append("A3 缺章节: " + ", ".join(miss3[:5]))
102
+
103
+ return {
104
+ "passed": passed,
105
+ "pct": round(pct, 1),
106
+ "total": total,
107
+ "full": full,
108
+ "a1": a1,
109
+ "a2": a2,
110
+ "a3": a3,
111
+ "r1": r1,
112
+ "r2": r2,
113
+ "r3": r3,
114
+ "msg1": msg1,
115
+ "msg2": msg2,
116
+ "msg3": msg3,
117
+ "misses": misses,
118
+ "prd": os.path.basename(prd_path),
119
+ }
120
+
121
+
122
+ def format_report(result):
123
+ """把 evaluate() 的结果格式化成人类可读的多行报告 (eval_prd CLI 用)。"""
124
+ lines = ["=== EVA 评估: {} ===".format(result["prd"])]
125
+ lines.append("")
126
+ lines.append("A1 现实锚定: {}/40 ({})".format(result["a1"], result["msg1"]))
127
+ if result["r2"] is not None:
128
+ lines.append("")
129
+ lines.append("A2 风格保真: {}/30 ({})".format(result["a2"], result["msg2"]))
130
+ else:
131
+ lines.append("")
132
+ lines.append("A2 风格保真: 跳过 (未提供原型文件)")
133
+ lines.append("")
134
+ lines.append("A3 模板完整: {}/30 ({})".format(result["a3"], result["msg3"]))
135
+
136
+ # 详细 misses
137
+ if result.get("msg1") and "找不到" in str(result.get("msg1", "")):
138
+ pass # msg 已含信息
139
+ lines.append("")
140
+ lines.append("总分: {}/{} ({:.0f}%)".format(
141
+ result["total"], result["full"], result["pct"]))
142
+ verdict = "PASS - 可以发布" if result["passed"] else "FAIL - 修复上述问题后再发布"
143
+ lines.append("结论: " + verdict)
144
+ return "\n".join(lines)