@hupan56/wlkj 2.7.12 → 3.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 (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -0,0 +1,223 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ result.py - 统一结果类型 + 优雅错误处理
4
+
5
+ 治"容错率低"的底层基建。现状各脚本错误风格极不统一:
6
+ - 裸 except Exception: pass 吞异常 (build_style_index / eval_prd / team_sync)
7
+ - exit code 含义不一致 (eval_prd 1=不达标或文件不存在; team_sync 0/1/2/3 四档)
8
+ - 调用方只能 parse 中文文案, 无法程序化决策
9
+
10
+ 本模块引入:
11
+ 1. Result 数据类: ok / code / msg / data, 语义化 code
12
+ 2. graceful_error 装饰器: 把 Result/异常翻译成"人类/AI 可读文本 + 一致 exit code"
13
+ 3. 统一 exit code 表 (替代散落各处的魔法数字)
14
+
15
+ 设计原则 (与方案一致):
16
+ - 失败不崩, 输出可读诊断 + 建议下一步
17
+ - 调用方能据 code 程序化决策, 不靠 parse 中文
18
+ - eval_prd 门禁从"硬阻塞"改"软门禁" (WARN 带警告发布)
19
+
20
+ Usage:
21
+ from common.result import Result, OK, fail, warn, graceful_error
22
+
23
+ @graceful_error
24
+ def main():
25
+ r = do_something()
26
+ if not r.ok:
27
+ return r # 装饰器翻译成可读输出 + exit code
28
+ return Result.ok(data=r.data)
29
+
30
+ Exit code 约定 (全脚本统一, 替代散落魔法数字):
31
+ 0 OK 成功
32
+ 1 GENERIC 通用失败 (未分类)
33
+ 2 BUSY 资源占用 (如 kg_db 被占)
34
+ 3 CONFLICT 冲突需人工 (如 team_sync SYNC_CONFLICT)
35
+ 4 AUTHZ 身份/权限拒绝 (与 identity.EXIT_AUTHZ 对齐)
36
+ 5 NOT_FOUND 文件/资源不存在
37
+ 6 INDEX_MISSING 索引缺失 (需先 init/doctor)
38
+ 7 BELOW_THRESH 门禁未达标 (eval_prd, 软门禁可带警告继续)
39
+ 8 NETWORK 网络/外部服务不可达
40
+ 9 DEPS_MISSING 依赖未装 (pip 包缺失)
41
+ """
42
+
43
+ import functools
44
+ import sys
45
+ import traceback
46
+ from dataclasses import dataclass, field
47
+ from typing import Any, Optional
48
+
49
+ __all__ = [
50
+ "Result",
51
+ "OK",
52
+ "ResultError",
53
+ "ok",
54
+ "fail",
55
+ "warn",
56
+ "graceful_error",
57
+ "EXIT",
58
+ "code_to_exit",
59
+ "CODE",
60
+ ]
61
+
62
+
63
+ # ============================================================
64
+ # 语义化 code 常量 (调用方据此程序化决策)
65
+ # ============================================================
66
+
67
+ class CODE:
68
+ """结果 code 常量。小写字符串, 语义化, 跨脚本一致。"""
69
+ OK = "ok"
70
+ GENERIC = "generic" # 通用未分类
71
+ BUSY = "busy" # 资源占用 (kg_db 锁)
72
+ CONFLICT = "conflict" # 冲突需人工
73
+ AUTHZ = "authz" # 身份/权限
74
+ NOT_FOUND = "not_found" # 文件/资源不存在
75
+ INDEX_MISSING = "index_missing" # 索引缺失
76
+ BELOW_THRESH = "below_thresh" # 门禁未达标
77
+ NETWORK = "network" # 网络不可达
78
+ DEPS_MISSING = "deps_missing" # 依赖未装
79
+
80
+
81
+ # ============================================================
82
+ # code → exit code 映射 (一处定义, 全脚本统一)
83
+ # ============================================================
84
+
85
+ EXIT = {
86
+ CODE.OK: 0,
87
+ CODE.GENERIC: 1,
88
+ CODE.BUSY: 2,
89
+ CODE.CONFLICT: 3,
90
+ CODE.AUTHZ: 4,
91
+ CODE.NOT_FOUND: 5,
92
+ CODE.INDEX_MISSING: 6,
93
+ CODE.BELOW_THRESH: 7,
94
+ CODE.NETWORK: 8,
95
+ CODE.DEPS_MISSING: 9,
96
+ }
97
+
98
+
99
+ def code_to_exit(code: str) -> int:
100
+ """code → exit code, 未知 code 回退 1。"""
101
+ return EXIT.get(code, 1)
102
+
103
+
104
+ # ============================================================
105
+ # Result 数据类
106
+ # ============================================================
107
+
108
+ @dataclass
109
+ class Result:
110
+ """统一结果。
111
+
112
+ Attributes:
113
+ ok: 是否成功
114
+ code: 语义化 code (见 CODE 常量), 调用方据此决策
115
+ msg: 人类可读消息
116
+ data: 成功时携带的数据 (任意)
117
+ hint: 失败时的"下一步建议" (可选, 输出给用户/AI)
118
+ """
119
+ ok: bool
120
+ code: str = CODE.GENERIC
121
+ msg: str = ""
122
+ data: Any = None
123
+ hint: Optional[str] = None
124
+
125
+ # ---- 工厂方法 ----
126
+ @classmethod
127
+ def success(cls, data: Any = None, msg: str = "", code: str = CODE.OK) -> "Result":
128
+ return cls(ok=True, code=code, msg=msg, data=data)
129
+
130
+ @classmethod
131
+ def failure(cls, msg: str, code: str = CODE.GENERIC,
132
+ hint: Optional[str] = None, data: Any = None) -> "Result":
133
+ return cls(ok=False, code=code, msg=msg, hint=hint, data=data)
134
+
135
+ @classmethod
136
+ def warning(cls, msg: str, code: str = CODE.BELOW_THRESH,
137
+ hint: Optional[str] = None, data: Any = None) -> "Result":
138
+ """WARN: 软门禁/带警告继续。ok=True (可继续), 但带警告。"""
139
+ return cls(ok=True, code=code, msg=msg, hint=hint, data=data)
140
+
141
+ # ---- 便捷判断 ----
142
+ @property
143
+ def is_warn(self) -> bool:
144
+ """是否为"成功但有警告"(如门禁 WARN 档)。"""
145
+ return self.ok and self.code != CODE.OK
146
+
147
+ def __bool__(self) -> bool:
148
+ return self.ok
149
+
150
+
151
+ # 模块级便捷别名 (常见用法)
152
+ OK = Result.success()
153
+ ResultError = type("ResultError", (Exception,), {}) # 预留: 未来 raise Result(r)
154
+
155
+
156
+ def ok(data: Any = None, msg: str = "") -> Result:
157
+ return Result.success(data=data, msg=msg)
158
+
159
+
160
+ def fail(msg: str, code: str = CODE.GENERIC, hint: Optional[str] = None) -> Result:
161
+ return Result.failure(msg=msg, code=code, hint=hint)
162
+
163
+
164
+ def warn(msg: str, code: str = CODE.BELOW_THRESH, hint: Optional[str] = None) -> Result:
165
+ return Result.warning(msg=msg, code=code, hint=hint)
166
+
167
+
168
+ # ============================================================
169
+ # graceful_error 装饰器
170
+ # ============================================================
171
+
172
+ def _emit(r: Result, *, exit_on_fail: bool = True) -> int:
173
+ """把 Result 翻译成可读输出 + 返回 exit code。"""
174
+ if r.ok and not r.is_warn:
175
+ if r.msg:
176
+ print(r.msg)
177
+ return 0
178
+ if r.is_warn:
179
+ # WARN: 打印警告但不当失败
180
+ print("[WARN] {}".format(r.msg))
181
+ if r.hint:
182
+ print(" -> {}".format(r.hint))
183
+ return code_to_exit(r.code) if False else 0 # WARN 视为成功 (exit 0)
184
+ # FAIL
185
+ tag = r.code.upper() if r.code != CODE.GENERIC else "ERROR"
186
+ print("[{}] {}".format(tag, r.msg))
187
+ if r.hint:
188
+ print(" -> {}".format(r.hint))
189
+ if exit_on_fail:
190
+ sys.exit(code_to_exit(r.code))
191
+ return code_to_exit(r.code)
192
+
193
+
194
+ def graceful_error(func):
195
+ """装饰 main(): 统一把返回值 (Result/None/int) 翻译成可读输出 + exit code。
196
+
197
+ - 返回 Result: 按 ok/code 翻译
198
+ - 返回 int: 直接当 exit code
199
+ - 返回 None: 视为成功 (exit 0)
200
+ - 抛异常: 捕获, 输出诊断 + traceback (失败不崩 → 但 main 层崩溃要可见)
201
+ """
202
+ @functools.wraps(func)
203
+ def wrapper(*args, **kwargs):
204
+ try:
205
+ ret = func(*args, **kwargs)
206
+ except SystemExit:
207
+ raise
208
+ except KeyboardInterrupt:
209
+ print("\n[中断] 用户取消")
210
+ sys.exit(130)
211
+ except Exception as e:
212
+ # 未捕获异常: 输出诊断 + 简短 traceback, 不静默吞
213
+ print("[ERROR] 未预期异常: {}".format(e))
214
+ print(" -> 这是 bug, 请反馈。traceback:")
215
+ traceback.print_exc()
216
+ sys.exit(1)
217
+ if isinstance(ret, Result):
218
+ return _emit(ret)
219
+ if isinstance(ret, int):
220
+ sys.exit(ret)
221
+ # None / 其他: 成功
222
+ return 0
223
+ return wrapper
@@ -0,0 +1,60 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ roles.py - 角色章节过滤 (单一真源, 消除 kg.py↔kg_mcp_server.py 逐字重复)
4
+
5
+ 现状: ROLE_SECTIONS 字典 + _filter_by_role 函数在 kg.py(:645-672) 和
6
+ kg_mcp_server.py(:249-276) 逐字重复。改一处忘改另一处 → CLI 和 MCP 行为分叉。
7
+
8
+ 本模块作为单一真源, kg.py 和 kg_mcp_server.py 都从这里 import。
9
+
10
+ 过滤规则: 按角色裁剪 context_pack 输出的章节 (## 1. / ## 2. / ## 2b. 等),
11
+ 让不同角色只看到与其职责相关的段落, 减少噪音。
12
+
13
+ Usage:
14
+ from common.roles import filter_by_role, ROLE_SECTIONS
15
+ text = filter_by_role(context_pack_output, role='pm')
16
+ """
17
+
18
+ import re
19
+
20
+ __all__ = ["ROLE_SECTIONS", "filter_by_role"]
21
+
22
+ # 各角色可见的 context_pack 章节号
23
+ # (与 kg.py ROLE_SECTIONS / kg_mcp_server.py ROLE_SECTIONS 对齐, 此处为唯一真源)
24
+ ROLE_SECTIONS = {
25
+ 'pm': {'1', '2', '2b', '3', '4', '5', '7'}, # PM: 字段+PRD+API,无代码噪音
26
+ 'design': {'2', '2b', '3', '6'}, # 设计: 页面+布局+字段+风格
27
+ 'dev': {'1', '3', '5', '7'}, # 开发: 代码+字段+API+wiki
28
+ 'test': {'1', '4', '5'}, # 测试: 代码+PRD+API
29
+ 'admin': {'4', '5'}, # 管理: PRD+API 概览
30
+ }
31
+
32
+
33
+ def filter_by_role(text, role):
34
+ """按角色过滤 context_pack 输出的章节。
35
+
36
+ Args:
37
+ text: context_pack 的多段输出 (## 1. xxx / ## 2. xxx ...)
38
+ role: 角色名 (pm/design/dev/test/admin), 未知角色不过滤
39
+
40
+ Returns:
41
+ 过滤后的文本 (只保留该角色可见章节)
42
+ """
43
+ visible = ROLE_SECTIONS.get((role or '').lower())
44
+ if not visible:
45
+ return text # 未知角色不过滤
46
+ # 按 "## 数字." 切段 (含 2b 这种带字母的)
47
+ sections = re.split(r'(\n## \d+[ab]?\.)', text)
48
+ result = sections[0]
49
+ i = 1
50
+ while i < len(sections):
51
+ header = sections[i]
52
+ num_m = re.match(r'\n## (\d+[ab]?)\.', header)
53
+ if num_m and num_m.group(1) in visible:
54
+ result += header
55
+ if i + 1 < len(sections):
56
+ result += sections[i + 1]
57
+ i += 2
58
+ else:
59
+ i += 2
60
+ return result.strip()
@@ -173,16 +173,28 @@ def modify_task_json(task_dir: Path, mutator, timeout: float = 15.0):
173
173
 
174
174
  lock_path = task_dir / ".task.lock"
175
175
  lock = FileLock(str(lock_path), timeout=timeout, stale_seconds=300)
176
- try:
177
- lock.acquire()
178
- except LockTimeoutError as e:
179
- print(f"Warning: task lock busy ({task_dir.name}): {e}", file=sys.stderr)
180
- # 降级: 不阻塞, 直接写 (好过完全失败)
181
- data = load_task_json(task_dir)
182
- if data is None:
176
+
177
+ # 锁竞争超时: 退避重试一次。绝不降级为无锁写 —— modify_task_json 存在的意义
178
+ # 就是防并发 read-modify-write 丢更新, 两个并发方都超时降级 = 必然丢一个的更新。
179
+ acquired = False
180
+ for attempt in range(2):
181
+ try:
182
+ lock.acquire()
183
+ acquired = True
184
+ break
185
+ except LockTimeoutError as e:
186
+ if attempt == 0:
187
+ # 第一次超时: 短暂退避后重试 (锁持有方通常很快释放)
188
+ import time as _time
189
+ _time.sleep(0.5)
190
+ continue
191
+ # 重试仍超时: 明确失败, 不写 (宁可这次改不了, 也不丢更新)
192
+ print(f"ERROR: task lock busy after retry ({task_dir.name}): {e}", file=sys.stderr)
193
+ print(f" 另一个进程正在修改此任务, 请稍后重试。改动未生效 (未丢数据)。", file=sys.stderr)
183
194
  return False
184
- mutator(data)
185
- return write_task_json(task_dir, data)
195
+ if not acquired:
196
+ return False
197
+
186
198
  try:
187
199
  data = load_task_json(task_dir)
188
200
  if data is None:
@@ -0,0 +1,115 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Test code extractor — E1: extract structured assertions from Test.java files.
4
+
5
+ These are the "second layer" of the knowledge graph: structured business
6
+ knowledge (Given-When-Then, assertion patterns, service dependencies) that
7
+ production code extraction can never reach. 39+ Test.java files in fywl-ics
8
+ contain real integration tests with @Autowired services and assertion logic.
9
+
10
+ JOIN keys to other layers:
11
+ - injected service class → code-callgraph (who this test exercises)
12
+ - package path → ui-modules / prd-index (which business module)
13
+ - @Test method name → code-keyword (searchable)
14
+ """
15
+ import os
16
+ import re
17
+
18
+ # @Test annotation (Junit 5)
19
+ RE_TEST = re.compile(r'@Test\b', re.M)
20
+ # @DisplayName("...")
21
+ RE_DISPLAY = re.compile(r'@DisplayName\s*\(\s*["\']([^"\']+)["\']\s*\)')
22
+ # method signature: void methodName() or public void methodName()
23
+ RE_METHOD = re.compile(r'(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|\w+(?:<[^>]*>)?)\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+[\w.,\s]+)?\s*\{', re.M)
24
+ # @Autowired / @Resource field: @Autowired private SomeService svc;
25
+ RE_INJECT = re.compile(r'@(?:Autowired|Resource)\s+(?:private\s+|protected\s+)?(\w+(?:<[^>]*>)?)\s+(\w+)\s*;', re.M)
26
+ # @SpringBootTest, @RunWith, @ExtendWith — marks integration tests
27
+ RE_INTEGRATION = re.compile(r'@(?:SpringBootTest|RunWith|ExtendWith|WebMvcTest|DataJpaTest)', re.M)
28
+ # Assertions: assertThat(x).isEqualTo(y), Assertions.assertEquals(x,y), Assert.X
29
+ RE_ASSERT = re.compile(r'(?:assertThat|Assertions?|Assert)\.(\w+)\s*\(', re.M)
30
+ # package declaration
31
+ RE_PACKAGE = re.compile(r'package\s+([\w.]+)\s*;', re.M)
32
+
33
+
34
+ def extract_test_assertions(proj_path):
35
+ """Scan a project for *Test.java files and extract structured test info.
36
+
37
+ Returns a list of dicts:
38
+ {file, class, package, method, display_name, injected_deps[],
39
+ assertions[], is_integration, comment}
40
+ """
41
+ results = []
42
+ if not proj_path or not os.path.isdir(proj_path):
43
+ return results
44
+
45
+ for root, dirs, files in os.walk(proj_path):
46
+ dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist', '__pycache__')]
47
+ for f in files:
48
+ if not f.endswith('Test.java'):
49
+ continue
50
+ fpath = os.path.join(root, f)
51
+ try:
52
+ content = open(fpath, encoding='utf-8', errors='ignore').read()
53
+ except Exception:
54
+ continue
55
+ if '@Test' not in content:
56
+ continue
57
+
58
+ # Class-level info
59
+ pkg_m = RE_PACKAGE.search(content)
60
+ package = pkg_m.group(1) if pkg_m else ''
61
+ is_integration = bool(RE_INTEGRATION.search(content))
62
+
63
+ # Injected dependencies (services/mappers under test)
64
+ injected = []
65
+ for im in RE_INJECT.finditer(content):
66
+ dep_type = im.group(1).split('<')[0] # strip generics
67
+ injected.append({'type': dep_type, 'name': im.group(2)})
68
+
69
+ # Class name (from filename, strip .java)
70
+ class_name = f[:-5]
71
+
72
+ # Extract each @Test method
73
+ test_positions = [m.start() for m in RE_TEST.finditer(content)]
74
+ display_map = {}
75
+ for dm in RE_DISPLAY.finditer(content):
76
+ # Associate @DisplayName with the nearest @Test after it
77
+ display_map[dm.end()] = dm.group(1)
78
+
79
+ for tp in test_positions:
80
+ # Find display name (nearest before this @Test, within 100 chars)
81
+ display = ''
82
+ for pos, name in display_map.items():
83
+ if pos <= tp and tp - pos < 200:
84
+ display = name
85
+
86
+ # Find method name after @Test
87
+ search_region = content[tp:tp + 300]
88
+ method_m = RE_METHOD.search(search_region)
89
+ if not method_m:
90
+ continue
91
+ method_name = method_m.group(1)
92
+
93
+ # Extract method body for assertions
94
+ body_start = tp + method_m.end()
95
+ body = content[body_start:body_start + 600]
96
+ assertions = list(set(m.group(1) for m in RE_ASSERT.finditer(body)))
97
+
98
+ # Extract comment before @Test (/** ... */ or // ...)
99
+ before = content[max(0, tp - 200):tp]
100
+ comment_m = re.search(r'(?:/\*\*\s*\n\s*\*\s*([^*]+?)\s*\n)|(?://\s*(.+)$)', before, re.M)
101
+ comment = (comment_m.group(1) or comment_m.group(2)).strip() if comment_m else ''
102
+
103
+ results.append({
104
+ 'file': fpath.replace('\\', '/'),
105
+ 'class': class_name,
106
+ 'package': package,
107
+ 'method': method_name,
108
+ 'display_name': display,
109
+ 'comment': comment[:80],
110
+ 'injected_deps': [d['type'] for d in injected[:5]],
111
+ 'assertions': assertions[:5],
112
+ 'is_integration': is_integration,
113
+ })
114
+
115
+ return results
@@ -0,0 +1,11 @@
1
+ # 子包初始化: 注入 scripts/根 + 所有子包到 sys.path
2
+ # 让跨包 import (旧式不带前缀) 仍可用, 兼容现有代码
3
+ import os as _os, sys as _sys
4
+ _root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
5
+ if _root not in _sys.path:
6
+ _sys.path.insert(0, _root)
7
+ # 注入所有子包目录 (兼容旧式 import xxx)
8
+ for _sub in ('kg', 'build', 'test', 'setup', 'io'):
9
+ _subdir = _os.path.join(_root, _sub)
10
+ if _os.path.isdir(_subdir) and _subdir not in _sys.path:
11
+ _sys.path.insert(0, _subdir)
@@ -0,0 +1,196 @@
1
+ # -*- coding: utf-8 -*-
2
+ """build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
3
+
4
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
+ import os as _o, sys as _s
6
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
7
+ if _cp not in _s.path: _s.path.insert(0, _cp)
8
+ from bootstrap import setup; setup()
9
+
10
+ 产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
11
+ 让 kg.py feature <功能名> 能返回功能画像。
12
+
13
+ 结构 (与 kg_duckdb.bulk_import_from_json 契约对齐):
14
+ {
15
+ "entities": {
16
+ "F:<dir>": {"type":"FEATURE","canonical":"<dir>","cn":"<中文名>","aliases":[...],"properties":{...}},
17
+ "E:<endpoint>": {"type":"ENDPOINT","canonical":"<endpoint>","cn":"","aliases":[],"properties":{}},
18
+ "C:<controller>": {"type":"CONTROLLER",...}
19
+ },
20
+ "feature_adjacency": {"F:<dir>": {"endpoints": ["E:<ep>", ...]}}
21
+ }
22
+
23
+ 数据源:
24
+ - ui-modules.json 的 modules (每个 module → FEATURE entity, cn=中文名)
25
+ - code-api.json 的 endpoints (每个 → ENDPOINT entity)
26
+ - trace-chain.json 的 traces (推导 FEATURE → ENDPOINT 邻接)
27
+
28
+ 注意: 这是基础实现 (从模块结构推导)。更丰富的语义 (controller/test 关联、
29
+ 业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
30
+ 那些脚本未实现时不影响本脚本的 entity 基础层。
31
+ """
32
+ import json
33
+ import os
34
+ import sys
35
+ from datetime import datetime
36
+
37
+ # 路径自适应 (和兄弟脚本一致)
38
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
39
+ sys.path.insert(0, os.path.join(THIS_DIR, '..'))
40
+ from common.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
41
+
42
+
43
+ def load(name):
44
+ """从 _build_cache 读索引 JSON (build_style_index 的产物位置)。"""
45
+ for d in (str(BUILD_CACHE_DIR), str(DATA_INDEX_DIR)):
46
+ p = os.path.join(d, name)
47
+ if os.path.isfile(p):
48
+ try:
49
+ return json.load(open(p, encoding='utf-8'))
50
+ except (json.JSONDecodeError, OSError):
51
+ pass
52
+ return {}
53
+
54
+
55
+ def build():
56
+ entities = {}
57
+ aliases_rows = [] # (alias_lower, entity_id)
58
+ feature_adjacency = {}
59
+
60
+ # 1. FEATURE entities (from ui-modules.json)
61
+ mi = load('ui-modules.json')
62
+ feat_count = 0
63
+ for proj, mods in mi.get('projects', {}).items():
64
+ for m in mods:
65
+ if not isinstance(m, dict):
66
+ continue
67
+ mdir = m.get('dir', '').strip()
68
+ if not mdir:
69
+ continue
70
+ eid = 'F:%s' % mdir
71
+ cn = m.get('cn', '').strip()
72
+ # cn 为空或与 dir 相同时不算有中文名
73
+ if not cn or cn == mdir:
74
+ cn = ''
75
+ entities[eid] = {
76
+ 'type': 'FEATURE',
77
+ 'canonical': mdir,
78
+ 'cn': cn,
79
+ 'aliases': [mdir, mdir.lower()],
80
+ 'properties': {'project': proj, 'path': m.get('path', '')},
81
+ }
82
+ for a in entities[eid]['aliases']:
83
+ aliases_rows.append((a.lower(), eid))
84
+ feature_adjacency[eid] = {'endpoints': [], 'buttons': []}
85
+ feat_count += 1
86
+
87
+ # 2. ENDPOINT entities (from code-api.json + trace-chain.json 补充)
88
+ # trace 的 endpoint 比 code-api 多且粒度不同, 两者都收录, 避免邻接匹配落空。
89
+ api = load('code-api.json')
90
+ tc = load('trace-chain.json')
91
+ ep_count = 0
92
+ for ep, ctrl_file in api.items():
93
+ eid = 'E:%s' % ep
94
+ entities[eid] = {
95
+ 'type': 'ENDPOINT',
96
+ 'canonical': ep,
97
+ 'cn': '',
98
+ 'aliases': [ep],
99
+ 'properties': {'controller_file': ctrl_file},
100
+ }
101
+ aliases_rows.append((ep.lower(), eid))
102
+ ep_count += 1
103
+ # 补充 trace 里独有的 endpoint
104
+ for t in tc.get('traces', []):
105
+ ep = t.get('endpoint', '')
106
+ if not ep:
107
+ continue
108
+ eid = 'E:%s' % ep
109
+ if eid not in entities:
110
+ entities[eid] = {
111
+ 'type': 'ENDPOINT',
112
+ 'canonical': ep,
113
+ 'cn': '',
114
+ 'aliases': [ep],
115
+ 'properties': {'source': 'trace', 'verb': t.get('verb', '')},
116
+ }
117
+ aliases_rows.append((ep.lower(), eid))
118
+ ep_count += 1
119
+
120
+ # 3. FEATURE -> ENDPOINT 邻接 (from trace-chain.json, tc 已在 step 2 加载)
121
+ # trace 里 button 的 vue_file 路径含 module dir, 可反推它属于哪个 FEATURE。
122
+ feat_canonicals = [(fid, ent['canonical']) for fid, ent in entities.items()
123
+ if ent['type'] == 'FEATURE' and ent.get('canonical')]
124
+ trace_matched = 0
125
+ for t in tc.get('traces', []):
126
+ vue = (t.get('vue_file') or '').replace('\\', '/')
127
+ endpoint = t.get('endpoint', '')
128
+ if not vue or not endpoint:
129
+ continue
130
+ # 从 vue 路径找匹配的 module dir (最长匹配优先, 避免 cost 误匹配 costDetail)
131
+ matched_feat = None
132
+ best_len = 0
133
+ for fid, d in feat_canonicals:
134
+ if ('/%s/' % d) in vue or vue.endswith('/%s' % d) or ('/%s.' % d) in vue:
135
+ if len(d) > best_len: # 最长匹配
136
+ matched_feat = fid
137
+ best_len = len(d)
138
+ if matched_feat:
139
+ trace_matched += 1
140
+ adj = feature_adjacency.setdefault(matched_feat, {})
141
+ adj.setdefault('endpoints', [])
142
+ adj.setdefault('buttons', [])
143
+ ep_eid = 'E:%s' % endpoint
144
+ if ep_eid in entities and ep_eid not in adj['endpoints']:
145
+ adj['endpoints'].append(ep_eid)
146
+ # button 邻接: vue_short:handler 归属此 feature
147
+ vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue
148
+ handler = t.get('handler', '')
149
+ btn_eid = 'B:%s:%s' % (vue_short, handler)
150
+ if btn_eid not in adj['buttons']:
151
+ adj['buttons'].append(btn_eid)
152
+
153
+ # 4. 去重 aliases
154
+ seen_alias = set()
155
+ unique_aliases = []
156
+ for a, eid in aliases_rows:
157
+ key = (a, eid)
158
+ if key not in seen_alias:
159
+ seen_alias.add(key)
160
+ unique_aliases.append({'alias': a, 'entity_id': eid})
161
+
162
+ registry = {
163
+ 'version': '1.0',
164
+ 'updated': datetime.now().isoformat(),
165
+ 'entities': entities,
166
+ 'aliases': unique_aliases,
167
+ 'feature_adjacency': feature_adjacency,
168
+ 'stats': {
169
+ 'features': feat_count,
170
+ 'endpoints': ep_count,
171
+ 'total_entities': len(entities),
172
+ 'features_with_cn': sum(1 for e in entities.values()
173
+ if e['type'] == 'FEATURE' and e.get('cn')),
174
+ 'trace_matched': trace_matched,
175
+ 'features_with_endpoints': sum(1 for v in feature_adjacency.values() if v.get('endpoints')),
176
+ },
177
+ }
178
+ return registry
179
+
180
+
181
+ def main():
182
+ print('Building entity registry...')
183
+ registry = build()
184
+ out = os.path.join(str(BUILD_CACHE_DIR), 'entity-registry.json')
185
+ os.makedirs(os.path.dirname(out), exist_ok=True)
186
+ with open(out, 'w', encoding='utf-8') as f:
187
+ json.dump(registry, f, ensure_ascii=False, indent=2, sort_keys=True)
188
+ s = registry['stats']
189
+ print(' FEATURE entities: %d (%d 有中文名)' % (s['features'], s['features_with_cn']))
190
+ print(' ENDPOINT entities: %d' % s['endpoints'])
191
+ print(' aliases: %d' % len(registry['aliases']))
192
+ print(' Saved to: %s' % out)
193
+
194
+
195
+ if __name__ == '__main__':
196
+ main()