@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -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,201 @@
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
+ _f = _o.path.abspath(__file__)
7
+ for _ in range(10):
8
+ _f = _o.path.dirname(_f)
9
+ _cp = _o.path.join(_f, 'foundation')
10
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
+ break
12
+ if _cp not in _s.path: _s.path.insert(0, _cp)
13
+ from bootstrap import setup; setup()
14
+
15
+ 产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
16
+ 让 kg.py feature <功能名> 能返回功能画像。
17
+
18
+ 结构 (与 kg_duckdb.bulk_import_from_json 契约对齐):
19
+ {
20
+ "entities": {
21
+ "F:<dir>": {"type":"FEATURE","canonical":"<dir>","cn":"<中文名>","aliases":[...],"properties":{...}},
22
+ "E:<endpoint>": {"type":"ENDPOINT","canonical":"<endpoint>","cn":"","aliases":[],"properties":{}},
23
+ "C:<controller>": {"type":"CONTROLLER",...}
24
+ },
25
+ "feature_adjacency": {"F:<dir>": {"endpoints": ["E:<ep>", ...]}}
26
+ }
27
+
28
+ 数据源:
29
+ - ui-modules.json 的 modules (每个 module → FEATURE entity, cn=中文名)
30
+ - code-api.json 的 endpoints (每个 → ENDPOINT entity)
31
+ - trace-chain.json 的 traces (推导 FEATURE → ENDPOINT 邻接)
32
+
33
+ 注意: 这是基础实现 (从模块结构推导)。更丰富的语义 (controller/test 关联、
34
+ 业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
35
+ 那些脚本未实现时不影响本脚本的 entity 基础层。
36
+ """
37
+ import json
38
+ import os
39
+ import sys
40
+ from datetime import datetime
41
+
42
+ # 路径自适应 (和兄弟脚本一致)
43
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
44
+ sys.path.insert(0, os.path.join(THIS_DIR, '..'))
45
+ from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
46
+
47
+
48
+ def load(name):
49
+ """从 _build_cache 读索引 JSON (build_style_index 的产物位置)。"""
50
+ for d in (str(BUILD_CACHE_DIR), str(DATA_INDEX_DIR)):
51
+ p = os.path.join(d, name)
52
+ if os.path.isfile(p):
53
+ try:
54
+ return json.load(open(p, encoding='utf-8'))
55
+ except (json.JSONDecodeError, OSError):
56
+ pass
57
+ return {}
58
+
59
+
60
+ def build():
61
+ entities = {}
62
+ aliases_rows = [] # (alias_lower, entity_id)
63
+ feature_adjacency = {}
64
+
65
+ # 1. FEATURE entities (from ui-modules.json)
66
+ mi = load('ui-modules.json')
67
+ feat_count = 0
68
+ for proj, mods in mi.get('projects', {}).items():
69
+ for m in mods:
70
+ if not isinstance(m, dict):
71
+ continue
72
+ mdir = m.get('dir', '').strip()
73
+ if not mdir:
74
+ continue
75
+ eid = 'F:%s' % mdir
76
+ cn = m.get('cn', '').strip()
77
+ # cn 为空或与 dir 相同时不算有中文名
78
+ if not cn or cn == mdir:
79
+ cn = ''
80
+ entities[eid] = {
81
+ 'type': 'FEATURE',
82
+ 'canonical': mdir,
83
+ 'cn': cn,
84
+ 'aliases': [mdir, mdir.lower()],
85
+ 'properties': {'project': proj, 'path': m.get('path', '')},
86
+ }
87
+ for a in entities[eid]['aliases']:
88
+ aliases_rows.append((a.lower(), eid))
89
+ feature_adjacency[eid] = {'endpoints': [], 'buttons': []}
90
+ feat_count += 1
91
+
92
+ # 2. ENDPOINT entities (from code-api.json + trace-chain.json 补充)
93
+ # trace 的 endpoint 比 code-api 多且粒度不同, 两者都收录, 避免邻接匹配落空。
94
+ api = load('code-api.json')
95
+ tc = load('trace-chain.json')
96
+ ep_count = 0
97
+ for ep, ctrl_file in api.items():
98
+ eid = 'E:%s' % ep
99
+ entities[eid] = {
100
+ 'type': 'ENDPOINT',
101
+ 'canonical': ep,
102
+ 'cn': '',
103
+ 'aliases': [ep],
104
+ 'properties': {'controller_file': ctrl_file},
105
+ }
106
+ aliases_rows.append((ep.lower(), eid))
107
+ ep_count += 1
108
+ # 补充 trace 里独有的 endpoint
109
+ for t in tc.get('traces', []):
110
+ ep = t.get('endpoint', '')
111
+ if not ep:
112
+ continue
113
+ eid = 'E:%s' % ep
114
+ if eid not in entities:
115
+ entities[eid] = {
116
+ 'type': 'ENDPOINT',
117
+ 'canonical': ep,
118
+ 'cn': '',
119
+ 'aliases': [ep],
120
+ 'properties': {'source': 'trace', 'verb': t.get('verb', '')},
121
+ }
122
+ aliases_rows.append((ep.lower(), eid))
123
+ ep_count += 1
124
+
125
+ # 3. FEATURE -> ENDPOINT 邻接 (from trace-chain.json, tc 已在 step 2 加载)
126
+ # trace 里 button 的 vue_file 路径含 module dir, 可反推它属于哪个 FEATURE。
127
+ feat_canonicals = [(fid, ent['canonical']) for fid, ent in entities.items()
128
+ if ent['type'] == 'FEATURE' and ent.get('canonical')]
129
+ trace_matched = 0
130
+ for t in tc.get('traces', []):
131
+ vue = (t.get('vue_file') or '').replace('\\', '/')
132
+ endpoint = t.get('endpoint', '')
133
+ if not vue or not endpoint:
134
+ continue
135
+ # 从 vue 路径找匹配的 module dir (最长匹配优先, 避免 cost 误匹配 costDetail)
136
+ matched_feat = None
137
+ best_len = 0
138
+ for fid, d in feat_canonicals:
139
+ if ('/%s/' % d) in vue or vue.endswith('/%s' % d) or ('/%s.' % d) in vue:
140
+ if len(d) > best_len: # 最长匹配
141
+ matched_feat = fid
142
+ best_len = len(d)
143
+ if matched_feat:
144
+ trace_matched += 1
145
+ adj = feature_adjacency.setdefault(matched_feat, {})
146
+ adj.setdefault('endpoints', [])
147
+ adj.setdefault('buttons', [])
148
+ ep_eid = 'E:%s' % endpoint
149
+ if ep_eid in entities and ep_eid not in adj['endpoints']:
150
+ adj['endpoints'].append(ep_eid)
151
+ # button 邻接: vue_short:handler 归属此 feature
152
+ vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue
153
+ handler = t.get('handler', '')
154
+ btn_eid = 'B:%s:%s' % (vue_short, handler)
155
+ if btn_eid not in adj['buttons']:
156
+ adj['buttons'].append(btn_eid)
157
+
158
+ # 4. 去重 aliases
159
+ seen_alias = set()
160
+ unique_aliases = []
161
+ for a, eid in aliases_rows:
162
+ key = (a, eid)
163
+ if key not in seen_alias:
164
+ seen_alias.add(key)
165
+ unique_aliases.append({'alias': a, 'entity_id': eid})
166
+
167
+ registry = {
168
+ 'version': '1.0',
169
+ 'updated': datetime.now().isoformat(),
170
+ 'entities': entities,
171
+ 'aliases': unique_aliases,
172
+ 'feature_adjacency': feature_adjacency,
173
+ 'stats': {
174
+ 'features': feat_count,
175
+ 'endpoints': ep_count,
176
+ 'total_entities': len(entities),
177
+ 'features_with_cn': sum(1 for e in entities.values()
178
+ if e['type'] == 'FEATURE' and e.get('cn')),
179
+ 'trace_matched': trace_matched,
180
+ 'features_with_endpoints': sum(1 for v in feature_adjacency.values() if v.get('endpoints')),
181
+ },
182
+ }
183
+ return registry
184
+
185
+
186
+ def main():
187
+ print('Building entity registry...')
188
+ registry = build()
189
+ out = os.path.join(str(BUILD_CACHE_DIR), 'entity-registry.json')
190
+ os.makedirs(os.path.dirname(out), exist_ok=True)
191
+ with open(out, 'w', encoding='utf-8') as f:
192
+ json.dump(registry, f, ensure_ascii=False, indent=2, sort_keys=True)
193
+ s = registry['stats']
194
+ print(' FEATURE entities: %d (%d 有中文名)' % (s['features'], s['features_with_cn']))
195
+ print(' ENDPOINT entities: %d' % s['endpoints'])
196
+ print(' aliases: %d' % len(registry['aliases']))
197
+ print(' Saved to: %s' % out)
198
+
199
+
200
+ if __name__ == '__main__':
201
+ main()
@@ -0,0 +1,132 @@
1
+ # -*- coding: utf-8 -*-
2
+ """build_relations.py — 从 trace-chain + code-api 推导关系边, 生成 relations.json。
3
+
4
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
+ import os as _o, sys as _s
6
+ _f = _o.path.abspath(__file__)
7
+ for _ in range(10):
8
+ _f = _o.path.dirname(_f)
9
+ _cp = _o.path.join(_f, 'foundation')
10
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
+ break
12
+ if _cp not in _s.path: _s.path.insert(0, _cp)
13
+ from bootstrap import setup; setup()
14
+
15
+ 产出 data/index/_build_cache/relations.json, 供 kg_duckdb 导入 edges 表,
16
+ 让 kg.py feature 的端点/按钮数据非零, kg.py hop 多跳遍历有边可走。
17
+
18
+ 结构 (与 kg_duckdb.bulk_import_from_json 的 edges_data 契约对齐):
19
+ {
20
+ "edges": {
21
+ "click_to_api": [
22
+ {"vue_file":..., "button":..., "handler":..., "endpoint":..., "confidence":...}
23
+ ],
24
+ "api_to_controller": {"<endpoint>": "<controller_file>"},
25
+ "controller_to_tests": {"<controller>": [{"test":..., "dep":..., "assertions":[...]}]}
26
+ }
27
+ }
28
+
29
+ 数据源:
30
+ - trace-chain.json: traces (vue_file→button→handler→api_fn→endpoint→verb→confidence)
31
+ api_functions (fnName→{url,verb,file})
32
+ - code-api.json: {endpoint: controller_file} (api_to_controller 直接用)
33
+ - test-assertions.json: tests (controller_to_tests 推导)
34
+ """
35
+ import json
36
+ import os
37
+ import sys
38
+ from datetime import datetime
39
+
40
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
41
+ sys.path.insert(0, os.path.join(THIS_DIR, '..'))
42
+ from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
43
+
44
+
45
+ def load(name):
46
+ for d in (str(BUILD_CACHE_DIR), str(DATA_INDEX_DIR)):
47
+ p = os.path.join(d, name)
48
+ if os.path.isfile(p):
49
+ try:
50
+ return json.load(open(p, encoding='utf-8'))
51
+ except (json.JSONDecodeError, OSError):
52
+ pass
53
+ return {}
54
+
55
+
56
+ def build():
57
+ tc = load('trace-chain.json')
58
+ api = load('code-api.json')
59
+
60
+ # 1. click_to_api: 直接从 traces 转 (字段名对齐 kg_duckdb 期望)
61
+ click_to_api = []
62
+ for t in tc.get('traces', []):
63
+ if not t.get('endpoint'):
64
+ continue
65
+ click_to_api.append({
66
+ 'vue_file': t.get('vue_file', ''),
67
+ 'button': t.get('button', ''),
68
+ 'handler': t.get('handler', ''),
69
+ 'api_fn': t.get('api_fn', ''),
70
+ 'endpoint': t.get('endpoint', ''),
71
+ 'verb': t.get('verb', ''),
72
+ 'confidence': t.get('confidence', ''),
73
+ })
74
+
75
+ # 2. api_to_controller: code-api.json 已经是 {endpoint: controller_file}
76
+ # 过滤掉空值
77
+ api_to_controller = {ep: f for ep, f in api.items() if ep and f}
78
+
79
+ # 3. controller_to_tests: 从 test-assertions.json 推导
80
+ # test 有 package/class 信息, controller 名从 controller_file 提取, 反向匹配 test
81
+ ta = load('test-assertions.json')
82
+ controller_to_tests = {}
83
+ # 建 controller 名 → test 的映射 (test 的 package 含 controller 关键字则关联)
84
+ ctrl_names = set()
85
+ for ctrl_file in api_to_controller.values():
86
+ cname = ctrl_file.split('/')[-1].replace('.java', '')
87
+ if cname:
88
+ ctrl_names.add(cname)
89
+ for t in ta.get('tests', []):
90
+ test_loc = (t.get('package', '') + '.' + t.get('class', '')).lower()
91
+ for cname in ctrl_names:
92
+ if cname.lower() in test_loc:
93
+ controller_to_tests.setdefault(cname, []).append({
94
+ 'test': t.get('display_name') or (t.get('class', '') + '.' + t.get('method', '')),
95
+ 'dep': '',
96
+ 'assertions': t.get('assertions', []),
97
+ })
98
+ break # 一个 test 只关联一个 controller
99
+
100
+ relations = {
101
+ 'version': '1.0',
102
+ 'updated': datetime.now().isoformat(),
103
+ 'edges': {
104
+ 'click_to_api': click_to_api,
105
+ 'api_to_controller': api_to_controller,
106
+ 'controller_to_tests': controller_to_tests,
107
+ },
108
+ 'stats': {
109
+ 'click_edges': len(click_to_api),
110
+ 'api_to_ctrl': len(api_to_controller),
111
+ 'ctrl_to_test': len(controller_to_tests),
112
+ },
113
+ }
114
+ return relations
115
+
116
+
117
+ def main():
118
+ print('Building relations...')
119
+ rel = build()
120
+ out = os.path.join(str(BUILD_CACHE_DIR), 'relations.json')
121
+ os.makedirs(os.path.dirname(out), exist_ok=True)
122
+ with open(out, 'w', encoding='utf-8') as f:
123
+ json.dump(rel, f, ensure_ascii=False, indent=2, sort_keys=True)
124
+ s = rel['stats']
125
+ print(' click_to_api edges: %d' % s['click_edges'])
126
+ print(' api_to_controller: %d' % s['api_to_ctrl'])
127
+ print(' controller_to_tests: %d' % s['ctrl_to_test'])
128
+ print(' Saved to: %s' % out)
129
+
130
+
131
+ if __name__ == '__main__':
132
+ main()
@@ -1,5 +1,17 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+ from foundation.core.paths import get_repo_root
14
+
3
15
  """
4
16
  Build style index from Vue/HTML files.
5
17
  Extracts UI patterns: tables, forms, layouts, colors, components.
@@ -8,7 +20,7 @@ Usage:
8
20
  python build_style_index.py
9
21
  """
10
22
  import os, json, re, sys
11
- from common.paths import BUILD_CACHE_DIR
23
+ from foundation.core.paths import BUILD_CACHE_DIR
12
24
  from datetime import datetime
13
25
 
14
26
  # UTF-8 stdio (防御性: stdout 被捕获时不崩溃)
@@ -20,7 +32,7 @@ except (AttributeError, TypeError, OSError, IOError):
20
32
  except Exception:
21
33
  pass
22
34
 
23
- BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
35
+ BASE = get_repo_root()
24
36
  DATA_CODE = os.path.join(BASE, 'data', 'code')
25
37
  INDEX_DIR = os.path.join(BASE, 'data', 'index')
26
38
  STYLE_INDEX_PATH = os.path.join(BUILD_CACHE_DIR, 'ui-style.json')
@@ -33,13 +45,20 @@ TEST_ASSERTIONS_PATH = os.path.join(BUILD_CACHE_DIR, 'test-assertions.json')
33
45
  # E2: prefer tree-sitter extraction (31%->70%+ coverage); fallback to regex
34
46
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
35
47
  try:
36
- from common.ts_extract import ts_build_api_fn_table as build_api_fn_table
37
- from common.ts_extract import ts_trace_clicks as trace_clicks
48
+ from domain.kg.extract.ts_extract import ts_build_api_fn_table as build_api_fn_table
49
+ from domain.kg.extract.ts_extract import ts_trace_clicks as trace_clicks
38
50
  _EXTRACT_ENGINE = 'tree-sitter'
39
51
  except Exception:
40
- from common.extract import build_api_fn_table, trace_clicks
52
+ from domain.kg.extract.extract import build_api_fn_table, trace_clicks
41
53
  _EXTRACT_ENGINE = 'regex (tree-sitter not installed)'
42
- from common.test_extract import extract_test_assertions # noqa: E402
54
+ # test_extract 是可选增强层 (test-assertions 索引), 模块可能未提供。
55
+ # 用 lazy import + 降级: 没有就跳过 test-assertions, 不阻断 ui-style 主线构建。
56
+ try:
57
+ from domain.kg.extract.test_extract import extract_test_assertions # noqa: E402
58
+ _HAS_TEST_EXTRACT = True
59
+ except ImportError:
60
+ extract_test_assertions = None
61
+ _HAS_TEST_EXTRACT = False
43
62
 
44
63
 
45
64
  def extract_vue_patterns(filepath, content):
@@ -377,7 +396,12 @@ def scan_project(proj_name, proj_path):
377
396
 
378
397
  for f in files:
379
398
  filepath = os.path.join(root, f)
380
- rel_path = os.path.relpath(filepath, proj_path).replace('\\', '/')
399
+ # relpath 在 Windows 上对特殊路径 (\\.\nul 等设备/跨盘符) 会抛 ValueError。
400
+ # 这些根本不是源码, 跳过即可, 不让一个异常文件炸掉整个扫描。
401
+ try:
402
+ rel_path = os.path.relpath(filepath, proj_path).replace('\\', '/')
403
+ except ValueError:
404
+ continue
381
405
 
382
406
  if f.endswith('.vue'):
383
407
  vue_count += 1
@@ -415,7 +439,7 @@ def scan_project(proj_name, proj_path):
415
439
  comp_m = re.findall(r"component:\s*['\"]([^'\"]+)['\"]", content)
416
440
  for comp in comp_m:
417
441
  form_components[comp] = form_components.get(comp, 0) + 1
418
- except:
442
+ except Exception:
419
443
  pass
420
444
 
421
445
  return {
@@ -662,13 +686,20 @@ def main():
662
686
 
663
687
  # test-assertions.json — E1: 测试代码提取 (Given-When-Then + 断言 + 注入依赖)
664
688
  # 第二层知识: 生产代码提取不到的业务预期和断言标准, 补上'怎么算对'天花板
665
- print('Building test assertions index...')
689
+ # 依赖 common.test_extract (可选模块); 缺失则跳过, 不阻断 ui-style 等主线。
666
690
  all_tests = []
667
- for proj in sorted(os.listdir(DATA_CODE)):
668
- proj_path = os.path.join(DATA_CODE, proj)
669
- if os.path.isdir(proj_path):
670
- tests = extract_test_assertions(proj_path)
671
- all_tests.extend(tests)
691
+ if not _HAS_TEST_EXTRACT:
692
+ print('Skipping test assertions index (common.test_extract not available - optional)')
693
+ else:
694
+ print('Building test assertions index...')
695
+ for proj in sorted(os.listdir(DATA_CODE)):
696
+ proj_path = os.path.join(DATA_CODE, proj)
697
+ if os.path.isdir(proj_path):
698
+ try:
699
+ tests = extract_test_assertions(proj_path)
700
+ all_tests.extend(tests)
701
+ except Exception as e:
702
+ print(' WARN: test_extract failed for {}: {}'.format(proj, str(e)[:80]))
672
703
  test_index = {
673
704
  'version': '1.0', 'updated': datetime.now().isoformat(),
674
705
  'tests': all_tests,
@@ -679,6 +710,8 @@ def main():
679
710
  json.dump(test_index, f, ensure_ascii=False, indent=2, sort_keys=True)
680
711
 
681
712
  # ── 知识图谱层: relations + features + entity-registry + DuckDB ──
713
+ # build_entity_registry 已实现 (FEATURE/ENDPOINT entity 基础层)。
714
+ # build_relations/features/workflows 是增强层, 未实现时 entities 表仍有基础数据。
682
715
  print('Building knowledge graph layers...')
683
716
  import subprocess
684
717
  for script in ['build_relations.py', 'build_features.py',
@@ -690,6 +723,8 @@ def main():
690
723
  print(' %s OK' % script)
691
724
  except Exception as e:
692
725
  print(' %s SKIP: %s' % (script, str(e)[:60]))
726
+ elif script != 'kg_duckdb.py':
727
+ print(' %s N/A (语义层脚本未实现, entities/workflows 表将空)' % script)
693
728
 
694
729
  print()
695
730
  print('=== Done ===')
@@ -0,0 +1,149 @@
1
+ # -*- coding: utf-8 -*-
2
+ """build_workflows.py — 从 traces + endpoints 推导业务流程链, 生成 workflow-index.json。
3
+
4
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
+ import os as _o, sys as _s
6
+ _f = _o.path.abspath(__file__)
7
+ for _ in range(10):
8
+ _f = _o.path.dirname(_f)
9
+ _cp = _o.path.join(_f, 'foundation')
10
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
+ break
12
+ if _cp not in _s.path: _s.path.insert(0, _cp)
13
+ from bootstrap import setup; setup()
14
+
15
+ 产出 data/index/_build_cache/workflow-index.json, 供 kg_duckdb 导入 workflows 表,
16
+ 让 kg.py workflow <模块> 能返回该模块的状态流转链 (query→create→audit→...)。
17
+
18
+ workflows 表结构: module, state, seq, button, endpoint
19
+
20
+ 推导逻辑 (基于 verb + 路径关键词的启发式状态推断):
21
+ - GET/read/list/page/export → "query" (查询)
22
+ - POST/add/create/save/submit/apply → "create" (新增/提交)
23
+ - PUT/update/edit/audit/approve/reject → "audit" (审核/修改)
24
+ - DELETE/remove/cancel/delete/del → "cancel" (取消/删除)
25
+
26
+ 数据源: trace-chain.json (vue_file→button→endpoint→verb) + entity-registry.json
27
+ (module→endpoint 邻接, 用于把 endpoint 归到 module)。
28
+ """
29
+ import json
30
+ import os
31
+ import sys
32
+ from datetime import datetime
33
+
34
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
35
+ sys.path.insert(0, os.path.join(THIS_DIR, '..'))
36
+ from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
37
+
38
+
39
+ def load(name):
40
+ for d in (str(BUILD_CACHE_DIR), str(DATA_INDEX_DIR)):
41
+ p = os.path.join(d, name)
42
+ if os.path.isfile(p):
43
+ try:
44
+ return json.load(open(p, encoding='utf-8'))
45
+ except (json.JSONDecodeError, OSError):
46
+ pass
47
+ return {}
48
+
49
+
50
+ # 状态推断规则 (按优先级, 先匹配先用)
51
+ STATE_RULES = [
52
+ ('query', ['get', 'read', 'list', 'page', 'export', 'detail', 'info', 'count']),
53
+ ('create', ['post', 'add', 'create', 'save', 'submit', 'apply', 'import', 'write']),
54
+ ('audit', ['put', 'update', 'edit', 'audit', 'approve', 'reject', 'review', 'confirm', 'send']),
55
+ ('cancel', ['delete', 'remove', 'cancel', 'del', 'revoke', 'close']),
56
+ ]
57
+ # 状态排序 (业务流程顺序)
58
+ STATE_ORDER = {'query': 1, 'create': 2, 'audit': 3, 'cancel': 4}
59
+
60
+
61
+ def infer_state(verb, endpoint):
62
+ """从 HTTP verb + endpoint 路径推断业务状态。"""
63
+ key = ((verb or '') + ' ' + (endpoint or '')).lower()
64
+ for state, patterns in STATE_RULES:
65
+ for p in patterns:
66
+ if p in key:
67
+ return state
68
+ return 'other'
69
+
70
+
71
+ def build():
72
+ er = load('entity-registry.json')
73
+ tc = load('trace-chain.json')
74
+
75
+ # module → [(state, button, endpoint)] 从 feature_adjacency + traces 推导
76
+ # feature_adjacency 已有 endpoints + buttons, 配合 trace 的 verb 推断 state
77
+ fa = er.get('feature_adjacency', {})
78
+ entities = er.get('entities', {})
79
+
80
+ # 建 endpoint → (verb, button) 映射 (从 traces)
81
+ ep_info = {} # endpoint -> {verb, buttons:set}
82
+ for t in tc.get('traces', []):
83
+ ep = t.get('endpoint', '')
84
+ if not ep:
85
+ continue
86
+ info = ep_info.setdefault(ep, {'verb': '', 'buttons': set()})
87
+ if t.get('verb') and not info['verb']:
88
+ info['verb'] = t.get('verb', '')
89
+ if t.get('button'):
90
+ info['buttons'].add(t.get('button'))
91
+
92
+ # 按 kg_duckdb.import_workflow_layer 期望的结构组装:
93
+ # {module: {chain: [{state, operations: [{button, endpoint}]}]}}
94
+ workflows = {}
95
+ module_states = {}
96
+ total_steps = 0
97
+ for feat_eid, adj in fa.items():
98
+ if not isinstance(adj, dict):
99
+ continue
100
+ ent = entities.get(feat_eid, {})
101
+ module = ent.get('canonical', feat_eid.replace('F:', ''))
102
+ eps = adj.get('endpoints', [])
103
+ if not eps:
104
+ continue
105
+ # 按 state 分组端点
106
+ state_ops = {} # state -> [{button, endpoint}]
107
+ for ep_eid in eps:
108
+ ep = ep_eid.replace('E:', '', 1)
109
+ info = ep_info.get(ep, {'verb': '', 'buttons': set()})
110
+ state = infer_state(info['verb'], ep)
111
+ btn = next(iter(info['buttons']), '') if info['buttons'] else ''
112
+ state_ops.setdefault(state, []).append({'button': btn, 'endpoint': ep})
113
+ # 按 STATE_ORDER 排序成 chain
114
+ chain = []
115
+ for state in sorted(state_ops.keys(), key=lambda s: STATE_ORDER.get(s, 5)):
116
+ chain.append({'state': state, 'operations': state_ops[state]})
117
+ total_steps += len(state_ops[state])
118
+ if chain:
119
+ workflows[module] = {'chain': chain}
120
+ module_states[module] = len(state_ops)
121
+
122
+ result = {
123
+ 'version': '1.0',
124
+ 'updated': datetime.now().isoformat(),
125
+ 'workflows': workflows,
126
+ 'stats': {
127
+ 'modules': len(module_states),
128
+ 'flow_steps': total_steps,
129
+ 'modules_with_multi_state': sum(1 for n in module_states.values() if n > 1),
130
+ },
131
+ }
132
+ return result
133
+
134
+
135
+ def main():
136
+ print('Building workflows...')
137
+ wf = build()
138
+ out = os.path.join(str(BUILD_CACHE_DIR), 'workflow-index.json')
139
+ os.makedirs(os.path.dirname(out), exist_ok=True)
140
+ with open(out, 'w', encoding='utf-8') as f:
141
+ json.dump(wf, f, ensure_ascii=False, indent=2, sort_keys=True)
142
+ s = wf['stats']
143
+ print(' modules: %d (%d 有多状态流转)' % (s['modules'], s['modules_with_multi_state']))
144
+ print(' flow steps: %d' % s['flow_steps'])
145
+ print(' Saved to: %s' % out)
146
+
147
+
148
+ if __name__ == '__main__':
149
+ main()