@hupan56/wlkj 3.1.32 → 3.3.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 (178) hide show
  1. package/bin/cli.js +117 -0
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -67
  4. package/templates/qoder/agents/prd-reference.md +47 -47
  5. package/templates/qoder/commands/optional/wl-insight.md +4 -4
  6. package/templates/qoder/commands/optional/wl-report.md +1 -1
  7. package/templates/qoder/commands/optional/wl-spec.md +23 -3
  8. package/templates/qoder/commands/optional/wl-status.md +12 -1
  9. package/templates/qoder/commands/wl-code.md +138 -7
  10. package/templates/qoder/commands/wl-commit.md +12 -1
  11. package/templates/qoder/commands/wl-design.md +70 -6
  12. package/templates/qoder/commands/wl-init.md +27 -0
  13. package/templates/qoder/commands/wl-prd.md +230 -15
  14. package/templates/qoder/commands/wl-req.md +10 -3
  15. package/templates/qoder/commands/wl-search.md +74 -20
  16. package/templates/qoder/commands/wl-task.md +3 -3
  17. package/templates/qoder/commands/wl-test.md +17 -2
  18. package/templates/qoder/contracts/CHANGELOG.md +418 -0
  19. package/templates/qoder/contracts/README.md +180 -0
  20. package/templates/qoder/contracts/code.md +82 -0
  21. package/templates/qoder/contracts/commit.md +86 -0
  22. package/templates/qoder/contracts/contract-header.md +76 -0
  23. package/templates/qoder/contracts/design.md +106 -0
  24. package/templates/qoder/contracts/fallback.md +126 -0
  25. package/templates/qoder/contracts/isolation.md +119 -0
  26. package/templates/qoder/contracts/prd.md +118 -0
  27. package/templates/qoder/contracts/schemas/design-spec.schema.json +46 -0
  28. package/templates/qoder/contracts/schemas/prd.schema.json +36 -0
  29. package/templates/qoder/contracts/schemas/test-cases.schema.json +40 -0
  30. package/templates/qoder/contracts/spec.md +116 -0
  31. package/templates/qoder/contracts/task.md +125 -0
  32. package/templates/qoder/contracts/test.md +112 -0
  33. package/templates/qoder/hooks/post-tool-use.py +61 -0
  34. package/templates/qoder/hooks/session-start.py +34 -66
  35. package/templates/qoder/hooks/stop-eval.py +47 -0
  36. package/templates/qoder/rules/wl-pipeline.md +37 -0
  37. package/templates/qoder/scripts/capability/__pycache__/__init__.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/capability/__pycache__/registry.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/capability/__pycache__/registry_mcp.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/capability/adapters/__init__.py +1 -1
  41. package/templates/qoder/scripts/capability/adapters/__pycache__/__init__.cpython-39.pyc +0 -0
  42. package/templates/qoder/scripts/capability/adapters/__pycache__/cli.cpython-39.pyc +0 -0
  43. package/templates/qoder/scripts/capability/adapters/__pycache__/mcp.cpython-39.pyc +0 -0
  44. package/templates/qoder/scripts/capability/adapters/__pycache__/qw.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/capability/adapters/mcp.py +76 -100
  46. package/templates/qoder/scripts/capability/adapters/qw.py +295 -295
  47. package/templates/qoder/scripts/capability/caps/__init__.py +1 -1
  48. package/templates/qoder/scripts/capability/caps/__pycache__/__init__.cpython-39.pyc +0 -0
  49. package/templates/qoder/scripts/capability/caps/__pycache__/context.cpython-39.pyc +0 -0
  50. package/templates/qoder/scripts/capability/caps/__pycache__/cron.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/capability/caps/__pycache__/identity.cpython-39.pyc +0 -0
  52. package/templates/qoder/scripts/capability/caps/__pycache__/memory.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/capability/caps/__pycache__/notify.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/capability/caps/__pycache__/present.cpython-39.pyc +0 -0
  55. package/templates/qoder/scripts/capability/caps/__pycache__/repo.cpython-39.pyc +0 -0
  56. package/templates/qoder/scripts/capability/caps/__pycache__/sandbox.cpython-39.pyc +0 -0
  57. package/templates/qoder/scripts/capability/caps/memory.py +1 -1
  58. package/templates/qoder/scripts/capability/registry.py +21 -23
  59. package/templates/qoder/scripts/capability/registry_mcp.py +69 -5
  60. package/templates/qoder/scripts/capability/smoke_test_report.json +34 -20
  61. package/templates/qoder/scripts/deployment/setup/carriers.py +3 -1
  62. package/templates/qoder/scripts/deployment/setup/init_doctor.py +10 -3
  63. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +11 -0
  64. package/templates/qoder/scripts/deployment/setup/wlkj_shim.py +104 -0
  65. package/templates/qoder/scripts/domain/__pycache__/__init__.cpython-39.pyc +0 -0
  66. package/templates/qoder/scripts/domain/deployment/deploy_to_test.py +298 -0
  67. package/templates/qoder/scripts/domain/integration/__init__.py +0 -0
  68. package/templates/qoder/scripts/domain/integration/__pycache__/__init__.cpython-39.pyc +0 -0
  69. package/templates/qoder/scripts/domain/integration/__pycache__/return_to_platform.cpython-39.pyc +0 -0
  70. package/templates/qoder/scripts/domain/integration/return_to_platform.py +392 -0
  71. package/templates/qoder/scripts/domain/integration/spec_upload.py +209 -0
  72. package/templates/qoder/scripts/domain/kg/build/kg_build.py +268 -25
  73. package/templates/qoder/scripts/domain/kg/build/kg_incremental.py +108 -3
  74. package/templates/qoder/scripts/domain/kg/build/kg_signatures.py +169 -0
  75. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  76. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  77. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  78. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  79. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  80. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  81. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  82. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  83. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  84. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  85. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  86. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  87. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  88. package/templates/qoder/scripts/domain/kg/extract/extract.py +84 -0
  89. package/templates/qoder/scripts/domain/kg/extract/extract.py.bak +430 -0
  90. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  91. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  92. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  93. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  94. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  95. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  96. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  97. package/templates/qoder/scripts/domain/kg/extract/java/__init__.py +15 -0
  98. package/templates/qoder/scripts/domain/kg/extract/java/_parser.py +271 -0
  99. package/templates/qoder/scripts/domain/kg/extract/java/all.py +145 -0
  100. package/templates/qoder/scripts/domain/kg/extract/java/build_java_to_pg.py +102 -0
  101. package/templates/qoder/scripts/domain/kg/extract/java/call_chain.py +49 -0
  102. package/templates/qoder/scripts/domain/kg/extract/java/class_extractor.py +141 -0
  103. package/templates/qoder/scripts/domain/kg/extract/java/domain_extractor.py +148 -0
  104. package/templates/qoder/scripts/domain/kg/extract/java/dubbo_extractor.py +33 -0
  105. package/templates/qoder/scripts/domain/kg/extract/java/endpoint_extractor.py +36 -0
  106. package/templates/qoder/scripts/domain/kg/extract/java/javadoc_extractor.py +110 -0
  107. package/templates/qoder/scripts/domain/kg/extract/java/llm_cn_filler.py +150 -0
  108. package/templates/qoder/scripts/domain/kg/extract/java/member_extractor.py +157 -0
  109. package/templates/qoder/scripts/domain/kg/extract/java/mybatisplus_extractor.py +34 -0
  110. package/templates/qoder/scripts/domain/kg/extract/java/pg_upsert.py +165 -0
  111. package/templates/qoder/scripts/domain/kg/extract/java/satoken_extractor.py +30 -0
  112. package/templates/qoder/scripts/domain/kg/extract/java/spring_extractor.py +39 -0
  113. package/templates/qoder/scripts/domain/kg/extract/java/validation_extractor.py +33 -0
  114. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  115. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  116. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  117. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  118. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  119. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  120. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  121. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  122. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  123. package/templates/qoder/scripts/domain/kg/extract/ts_extract.py +111 -0
  124. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +4 -2
  125. package/templates/qoder/scripts/domain/kg/kg.py +42 -5
  126. package/templates/qoder/scripts/domain/kg/search/_remote.py +187 -0
  127. package/templates/qoder/scripts/domain/kg/search/context_pack.py +32 -2
  128. package/templates/qoder/scripts/domain/kg/search/search_index.py +74 -20
  129. package/templates/qoder/scripts/domain/kg/storage/kg_duckdb.py +43 -0
  130. package/templates/qoder/scripts/domain/kg/switch_project.py +159 -0
  131. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  132. package/templates/qoder/scripts/domain/requirement/req.py +134 -28
  133. package/templates/qoder/scripts/domain/task/__pycache__/wlkj_panel.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -0
  135. package/templates/qoder/scripts/engine/poller.py +219 -0
  136. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  137. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  138. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  139. package/templates/qoder/scripts/foundation/core/paths.py +102 -0
  140. package/templates/qoder/scripts/foundation/integrations/active_task.py +2 -1
  141. package/templates/qoder/scripts/orchestration/wlkj.py +4 -0
  142. package/templates/qoder/scripts/protocol/__pycache__/__init__.cpython-39.pyc +0 -0
  143. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +24 -11
  144. package/templates/qoder/scripts/protocol/transports/__pycache__/__init__.cpython-39.pyc +0 -0
  145. package/templates/qoder/scripts/protocol/transports/__pycache__/base.cpython-39.pyc +0 -0
  146. package/templates/qoder/scripts/protocol/transports/__pycache__/cli.cpython-39.pyc +0 -0
  147. package/templates/qoder/scripts/protocol/transports/__pycache__/http.cpython-39.pyc +0 -0
  148. package/templates/qoder/scripts/protocol/transports/__pycache__/stdio.cpython-39.pyc +0 -0
  149. package/templates/qoder/scripts/protocol/transports/http.py +7 -1
  150. package/templates/qoder/scripts/validation/eval/qwork_harness.py +1 -1
  151. package/templates/qoder/scripts/validation/eval/report-commands.md +2 -2
  152. package/templates/qoder/settings.json +27 -9
  153. package/templates/qoder/skills/design-import/SKILL.md +3 -3
  154. package/templates/qoder/skills/design-review/SKILL.md +1 -1
  155. package/templates/qoder/skills/prd-generator/SKILL.md +4 -4
  156. package/templates/qoder/skills/prd-review/SKILL.md +1 -1
  157. package/templates/qoder/skills/prototype-generator/SKILL.md +3 -3
  158. package/templates/qoder/skills/spec-coder/SKILL.md +1 -1
  159. package/templates/qoder/skills/spec-generator/SKILL.md +80 -23
  160. package/templates/qoder/skills/test-generator/SKILL.md +1 -1
  161. package/templates/qoder/skills/wl-code/SKILL.md +13 -1
  162. package/templates/qoder/skills/wl-commit/SKILL.md +1 -1
  163. package/templates/qoder/skills/wl-design/SKILL.md +6 -6
  164. package/templates/qoder/skills/wl-init/SKILL.md +2 -2
  165. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  166. package/templates/qoder/skills/wl-prd/SKILL.md +60 -0
  167. package/templates/qoder/skills/wl-report/SKILL.md +2 -2
  168. package/templates/qoder/skills/wl-search/SKILL.md +1 -1
  169. package/templates/qoder/skills/wl-spec/SKILL.md +2 -2
  170. package/templates/qoder/skills/wl-status/SKILL.md +2 -2
  171. package/templates/qoder/skills/wl-task/SKILL.md +3 -3
  172. package/templates/qoder/skills/wl-test/SKILL.md +2 -2
  173. package/templates/qoder/templates/spec-template.md +124 -0
  174. package/templates/root/AGENTS.md +41 -14
  175. package/templates/qoder/scripts/domain/task/zentao_panel.py +0 -451
  176. package/templates/qoder/skills/wl-prd-full/SKILL.md +0 -121
  177. package/templates/qoder/skills/wl-prd-quick/SKILL.md +0 -50
  178. package/templates/qoder/skills/wl-prd-review/SKILL.md +0 -47
@@ -9,7 +9,7 @@ for _ in range(10):
9
9
  break
10
10
  if _cp not in _s.path: _s.path.insert(0, _cp)
11
11
  from bootstrap import setup; setup()
12
- from foundation.core.paths import get_repo_root
12
+ from foundation.core.paths import get_repo_root
13
13
 
14
14
  """
15
15
  kg.py — QODER 知识图谱统一 CLI 入口(全系列通用)
@@ -46,8 +46,8 @@ try:
46
46
  except Exception:
47
47
  pass
48
48
 
49
- SCRIPTS_DIR = get_repo_root()
50
- BASE_DIR = get_repo_root()
49
+ SCRIPTS_DIR = get_repo_root()
50
+ BASE_DIR = get_repo_root()
51
51
  sys.path.insert(0, SCRIPTS_DIR)
52
52
  _KG_DIR = os.path.dirname(os.path.abspath(__file__))
53
53
  if _KG_DIR not in sys.path: sys.path.insert(0, _KG_DIR) # 同包import
@@ -69,8 +69,10 @@ def _kg_db_path():
69
69
 
70
70
 
71
71
  def _has_duckdb():
72
- """知识图谱 DuckDB 是否已构建。"""
73
- return os.path.isfile(_kg_db_path())
72
+ """知识图谱本地 DuckDB 是否已构建。
73
+ 全迁 PG 后本地不再有 DuckDB——这个函数返回 False,让所有旧路径走 MCP 远程。
74
+ """
75
+ return False # 本地零 DuckDB,全部走 MCP → 工作台 PG
74
76
 
75
77
 
76
78
  def _hint_rebuild(action_cn='知识图谱'):
@@ -821,6 +823,36 @@ def _try_daemon(cmd, rest):
821
823
  return True
822
824
 
823
825
 
826
+ def _undo_msys_pathconv(arg):
827
+ """Undo Git Bash (MSYS2) path conversion on a single argument.
828
+
829
+ On Windows Git Bash, a leading-slash token like '/insurance' is
830
+ auto-rewritten to '<git-install>/insurance' (e.g. D:/programming/tools/
831
+ Git/Git/insurance) before Python ever sees argv. kg.py endpoints start
832
+ with '/', so every '/xxx' query is corrupted in that shell.
833
+
834
+ Detection: arg looks like an absolute Windows path (X:/...) AND contains
835
+ '/Git/' — then the real intent was '/<tail>'. Safe no-op for genuine paths
836
+ (file commands pass real paths that rarely contain /Git/, and the worst
837
+ case is a real path stays unchanged).
838
+ """
839
+ if not arg or len(arg) < 2 or arg[1] != ':':
840
+ return arg
841
+ # e.g. D:/programming/tools/Git/Git/insurance -> /insurance
842
+ # Use rfind: the install path itself may contain "Git" (e.g. .../Git/Git/),
843
+ # so the LAST /Git/ is the one MSYS inserts.
844
+ marker = '/Git/'
845
+ idx = arg.rfind(marker)
846
+ if idx < 0:
847
+ return arg
848
+ tail = arg[idx + len(marker):]
849
+ # Only restore when the tail looks like an endpoint fragment (no drive,
850
+ # no '.ext' that would indicate a real file path).
851
+ if ':' in tail or ('.' in tail and tail.rfind('.') > tail.rfind('/')):
852
+ return arg
853
+ return '/' + tail
854
+
855
+
824
856
  def main(argv=None):
825
857
  argv = argv if argv is not None else sys.argv[1:]
826
858
 
@@ -830,6 +862,11 @@ def main(argv=None):
830
862
  use_daemon = False
831
863
  argv = [a for a in argv if a != '--no-daemon']
832
864
 
865
+ # Undo MSYS path conversion on arguments (Git Bash rewrites '/xxx' →
866
+ # '<git-install>/xxx', which corrupts endpoint queries like /insurance).
867
+ if len(argv) > 1:
868
+ argv[1:] = [_undo_msys_pathconv(a) for a in argv[1:]]
869
+
833
870
  if not argv or argv[0] in ('-h', '--help', 'help'):
834
871
  print(_usage())
835
872
  return 0
@@ -0,0 +1,187 @@
1
+ """本地引擎 → 工作台 MCP 远程查询层。
2
+
3
+ 替代直连本地 kg.duckdb(已删),所有知识查询走 MCP HTTP → 工作台 PostgreSQL。
4
+ 下游命令(kg.py / wl-search / context_pack)零改动:函数签名不变,print 格式不变。
5
+
6
+ 用法(search_index.py 内):
7
+ from domain.kg.search._remote import call_remote, render_search_code
8
+ data = call_remote("search_code", {"keyword": query})
9
+ sys.stdout.write(render_search_code(data))
10
+ """
11
+ import json
12
+ import sys
13
+ import os
14
+
15
+ # McpCap 路径(相对于 .qoder/scripts/)
16
+ _scripts = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
17
+ if _scripts not in sys.path:
18
+ sys.path.insert(0, _scripts)
19
+
20
+ _mcp = None
21
+
22
+
23
+ def _get_mcp():
24
+ """惰性初始化 McpCap(首次调用连工作台 MCP server)。"""
25
+ global _mcp
26
+ if _mcp is None:
27
+ from capability.adapters.mcp import McpCap
28
+ _mcp = McpCap()
29
+ return _mcp
30
+
31
+
32
+ def call_remote(tool, args):
33
+ """调工作台 MCP 工具,返回 dict。失败抛 RuntimeError(无本地降级,kg.duckdb 已删)。"""
34
+ cap = _get_mcp()
35
+ result = cap.call(tool, args)
36
+ if result.is_error:
37
+ raise RuntimeError("MCP工具 %s 调用失败: %s" % (tool, getattr(result, 'text', '')[:200]))
38
+ try:
39
+ data = json.loads(result.text)
40
+ except Exception:
41
+ raise RuntimeError("MCP返回非JSON: %s" % result.text[:200])
42
+ if isinstance(data, dict) and data.get("degraded") and data.get("error"):
43
+ raise RuntimeError("工作台降级: %s" % data["error"])
44
+ return data
45
+
46
+
47
+ # ============================================================
48
+ # 渲染器:把 MCP JSON → 原本地 print 文本格式(下游命令零改动)
49
+ # ============================================================
50
+
51
+ def render_search_code(data):
52
+ """渲染 search_code 结果(仿 search_index.py _search_keywords_impl 格式)。"""
53
+ items = data.get("items", [])
54
+ if not items:
55
+ return "No matches.\n"
56
+ lines = ["[%d matches]" % len(items)]
57
+ for it in items[:20]:
58
+ t = it.get("type", "")
59
+ name = it.get("name", "")
60
+ label = it.get("label", "")
61
+ f = it.get("file", "")
62
+ plat = it.get("platform", "")
63
+ short = f.replace("\\", "/").split("/")[-1] if f else ""
64
+ suffix = " -> data/code/%s" % f.replace("\\", "/") if f else ""
65
+ line = " [%s] %s" % (t, name)
66
+ if label and label != name:
67
+ line += "(%s)" % label
68
+ if f:
69
+ line += " %s%s" % (short, suffix)
70
+ lines.append(line)
71
+ return "\n".join(lines) + "\n"
72
+
73
+
74
+ def render_search_api(data):
75
+ """渲染 search_api 结果。"""
76
+ items = data.get("items", [])
77
+ if not items:
78
+ return "No API matches.\n"
79
+ lines = ["[%d API matches]" % len(items)]
80
+ for it in items[:20]:
81
+ verb = (it.get("verb") or "").upper()
82
+ url = it.get("url", "")
83
+ fn = it.get("fn", "")
84
+ lines.append(" %s %s -> %s" % (verb or "GET", url, fn))
85
+ return "\n".join(lines) + "\n"
86
+
87
+
88
+ def render_search_field(data):
89
+ """渲染 search_field 结果。"""
90
+ items = data.get("items", [])
91
+ if not items:
92
+ return "No field matches.\n"
93
+ lines = ["[%d field matches]" % len(items)]
94
+ for it in items[:20]:
95
+ field = it.get("field", "")
96
+ title = it.get("title", "")
97
+ count = it.get("count", 0)
98
+ lines.append(" %s = %s (用%d次)" % (field, title, count))
99
+ return "\n".join(lines) + "\n"
100
+
101
+
102
+ def render_search_prds(data):
103
+ """渲染 PRD 搜索结果。"""
104
+ items = data.get("items", [])
105
+ if not items:
106
+ return "No PRD matches.\n"
107
+ lines = ["[%d PRD matches]" % len(items)]
108
+ for it in items[:10]:
109
+ title = it.get("title", "")
110
+ status = it.get("status", "")
111
+ lines.append(" [%s] %s" % (status, title))
112
+ return "\n".join(lines) + "\n"
113
+
114
+
115
+ def render_search_style(data):
116
+ """渲染风格搜索结果。"""
117
+ items = data.get("items", [])
118
+ if not items:
119
+ return "No style matches.\n"
120
+ lines = ["[%d style matches]" % len(items)]
121
+ for it in items[:10]:
122
+ lines.append(" %s: %s" % (it.get("title", ""), it.get("description", "")[:60]))
123
+ return "\n".join(lines) + "\n"
124
+
125
+
126
+ def render_impact(data):
127
+ """渲染影响分析结果。"""
128
+ entity = data.get("entity")
129
+ if not entity:
130
+ return "Entity not found.\n"
131
+ totals = data.get("totals", {})
132
+ by_type = data.get("byType", {})
133
+ lines = [
134
+ "影响分析: %s (%s)" % (entity.get("name", ""), entity.get("type", "")),
135
+ " 影响范围: %s 节点 / %s 关系" % (totals.get("nodes", 0), totals.get("links", 0)),
136
+ ]
137
+ if by_type:
138
+ lines.append(" 按类型: " + ", ".join("%s·%d" % (k, v) for k, v in by_type.items()))
139
+ for section in [("upstream", "⬆ 谁依赖它"), ("downstream", "⬇ 它依赖谁")]:
140
+ key, label = section
141
+ layers = data.get(key, [])
142
+ if layers:
143
+ lines.append(label)
144
+ for layer in layers:
145
+ depth = layer.get("depth", "?")
146
+ items = layer.get("items", [])
147
+ names = [it.get("name", "")[:30] for it in items[:5]]
148
+ lines.append(" L%s: %s" % (depth, ", ".join(names)))
149
+ if len(items) > 5:
150
+ lines.append(" +%d more" % (len(items) - 5))
151
+ return "\n".join(lines) + "\n"
152
+
153
+
154
+ def render_context360(data):
155
+ """渲染 context_360 结果。"""
156
+ parts = []
157
+ for key, label in [("code", "CODE"), ("api", "API"), ("field", "FIELD")]:
158
+ section = data.get(key, {})
159
+ # context_360 返回 {code: {items:[...], total:N}, ...} 不是直接 list
160
+ items = section.get("items", []) if isinstance(section, dict) else (section if isinstance(section, list) else [])
161
+ if items:
162
+ parts.append("[%s] %d matches" % (label, len(items)))
163
+ for it in items[:5]:
164
+ if isinstance(it, dict):
165
+ name = it.get("name", it.get("url", it.get("field", "")))
166
+ t = it.get("type", "")
167
+ extra = it.get("title", it.get("label", ""))
168
+ line = " %s %s" % (t, name) if t else " %s" % name
169
+ if extra and extra != name:
170
+ line += "(%s)" % extra
171
+ parts.append(line)
172
+ if not parts:
173
+ return "No context found.\n"
174
+ return "\n".join(parts) + "\n"
175
+
176
+
177
+ def render_rag_search(data):
178
+ """渲染 RAG 语义检索结果。"""
179
+ items = data.get("items", [])
180
+ if not items:
181
+ return "No semantic matches.\n"
182
+ lines = ["[%d semantic matches] vec=%s" % (len(items), data.get("vector_ok"))]
183
+ for it in items[:10]:
184
+ name = it.get("name", "")
185
+ score = it.get("vec_score", 0)
186
+ lines.append(" [%.2f] %s (%s)" % (score, name, it.get("type", "")))
187
+ return "\n".join(lines) + "\n"
@@ -198,8 +198,38 @@ def main():
198
198
 
199
199
 
200
200
  def _compute_and_print(query, platform, page_type):
201
- """实际的 context_pack 计算 (原 main 逻辑)。"""
202
-
201
+ """实际的 context_pack 计算 走工作台 MCP context_pack(本地 JSON 索引已废弃)。"""
202
+ try:
203
+ from domain.kg.search._remote import call_remote
204
+ data = call_remote("context_pack", {"query": query, "platform": platform})
205
+ # MCP context_pack 返回聚合结果,直接渲染
206
+ print('# Context Pack: {} (platform: {})'.format(query, platform or 'all'))
207
+ for section_key, section_label in [("code", "相关代码"), ("api", "API"),
208
+ ("field", "字段"), ("wiki", "Wiki"),
209
+ ("prd", "PRD")]:
210
+ items = data.get(section_key, [])
211
+ if not items:
212
+ continue
213
+ print('\n## %s (%d)' % (section_label, len(items)))
214
+ for it in items[:8]:
215
+ if isinstance(it, dict):
216
+ name = it.get("name", it.get("url", it.get("field", "")))
217
+ label = it.get("label", it.get("title", ""))
218
+ line = "- %s" % name
219
+ if label and label != name:
220
+ line += "(%s)" % label
221
+ print(line)
222
+ print("\n(数据来自工作台 PostgreSQL,MCP context_pack 工具)")
223
+ return
224
+ except Exception as e:
225
+ print("# Context Pack: MCP 调用失败(工作台未启动?)")
226
+ print("# 错误: %s" % str(e)[:200])
227
+ print("# 提示: 确保工作台后端在跑(start_pg.bat),mcp_config.json 配置正确")
228
+ return
229
+
230
+
231
+ def _compute_and_print_local(query, platform, page_type):
232
+ """[已废弃] 原本地 JSON 索引逻辑(kg.duckdb 已删,load_index 会失败)。"""
203
233
  targets = []
204
234
  if platform and platform.lower() not in ('both', '两端'):
205
235
  targets = [PLATFORM_MAP.get(platform.lower(), platform)]
@@ -413,6 +413,22 @@ def _check_index_freshness():
413
413
 
414
414
 
415
415
  def search_keywords(query, platform=None):
416
+ """搜代码关键词 → 走工作台 MCP search_code(本地 kg.duckdb 已删,不再直连)。"""
417
+ try:
418
+ from domain.kg.search._remote import call_remote, render_search_code
419
+ data = call_remote("search_code", {"keyword": query, "platform": platform})
420
+ sys.stdout.write(render_search_code(data))
421
+ return
422
+ except Exception as e:
423
+ print("MCP search_code 失败(工作台未启动?): %s" % str(e)[:200])
424
+ print("提示: 确保工作台后端在跑(start_pg.bat),mcp_config.json 配置正确")
425
+ return
426
+
427
+
428
+ def search_keywords_local(query, platform=None):
429
+ """[已废弃] 原本地直连 kg.duckdb 的搜索逻辑。保留仅供回退参考。
430
+ 本地 kg.duckdb 已删除,此函数会报 code-keyword.json not found。
431
+ """
416
432
  # 先查缓存 (命中即数据未变, 直接还)。缓存 key 已含 kg.duckdb mtime, 命中就说明
417
433
  # 数据源没变, 跳过 _check_index_freshness 的 os.walk —— 50 人团队每天几十次
418
434
  # 搜索, 每次都 walk 目录是纯浪费。未命中才做新鲜度检查 + 计算。
@@ -581,6 +597,17 @@ def _semantic_fallback(query, platform=None):
581
597
 
582
598
 
583
599
  def search_prds(query):
600
+ """搜 PRD → 走工作台 MCP search_prds。"""
601
+ try:
602
+ from domain.kg.search._remote import call_remote, render_search_prds
603
+ data = call_remote("search_prds", {"query": query})
604
+ sys.stdout.write(render_search_prds(data))
605
+ except Exception as e:
606
+ print("MCP search_prds 失败: %s" % str(e)[:200])
607
+
608
+
609
+ def search_prds_local(query):
610
+ """[已废弃] 原本地逻辑。"""
584
611
  prd_index = load_index('prd-index.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --prd-only')
585
612
  if prd_index is None:
586
613
  return
@@ -627,7 +654,17 @@ def search_prds(query):
627
654
 
628
655
 
629
656
  def search_style(style_type, platform=None):
630
- """Find pages of a given UI type (table/form/detail/modal/dashboard)."""
657
+ """搜风格 走工作台 MCP search_style。"""
658
+ try:
659
+ from domain.kg.search._remote import call_remote, render_search_style
660
+ data = call_remote("search_style", {"style_type": style_type, "platform": platform})
661
+ sys.stdout.write(render_search_style(data))
662
+ except Exception as e:
663
+ print("MCP search_style 失败: %s" % str(e)[:200])
664
+
665
+
666
+ def search_style_local(style_type, platform=None):
667
+ """[已废弃] 原本地逻辑。"""
631
668
  si = load_index('ui-style.json', hint='Run: python build_style_index.py')
632
669
  if si is None:
633
670
  return
@@ -663,7 +700,17 @@ def search_style(style_type, platform=None):
663
700
 
664
701
 
665
702
  def search_field(field_name):
666
- """Search field usage in ui-style.json field_map."""
703
+ """搜字段 走工作台 MCP search_field。"""
704
+ try:
705
+ from domain.kg.search._remote import call_remote, render_search_field
706
+ data = call_remote("search_field", {"name": field_name})
707
+ sys.stdout.write(render_search_field(data))
708
+ except Exception as e:
709
+ print("MCP search_field 失败: %s" % str(e)[:200])
710
+
711
+
712
+ def search_field_local(field_name):
713
+ """[已废弃] 原本地逻辑。"""
667
714
  si = load_index('ui-style.json', hint='Run: python build_style_index.py')
668
715
  if si is None:
669
716
  return
@@ -681,7 +728,17 @@ def search_field(field_name):
681
728
 
682
729
 
683
730
  def search_api(query):
684
- """Search API endpoints in code-api.json."""
731
+ """ API 走工作台 MCP search_api。"""
732
+ try:
733
+ from domain.kg.search._remote import call_remote, render_search_api
734
+ data = call_remote("search_api", {"keyword": query})
735
+ sys.stdout.write(render_search_api(data))
736
+ except Exception as e:
737
+ print("MCP search_api 失败: %s" % str(e)[:200])
738
+
739
+
740
+ def search_api_local(query):
741
+ """[已废弃] 原本地逻辑。"""
685
742
  ai = load_index('code-api.json', hint='Run: python .qoder/scripts/domain/task/git_sync.py --index-only')
686
743
  if ai is None:
687
744
  return
@@ -813,26 +870,23 @@ def search_wiki_cli(query):
813
870
 
814
871
 
815
872
  def search_impact(query):
816
- """改这个接口/符号影响哪些页面/按钮/模块? (U1: 内存图遍历)"""
817
- from domain.kg.graph.graph_traverse import CodeGraph
818
- g = CodeGraph(INDEX_DIR)
819
- result = g.impact(query)
820
- if result:
821
- print(result)
822
- else:
823
- print('未找到匹配 "{}" 的端点。尝试用部分路径, 如 /veh 或 export'.format(query))
824
- print('提示: trace-chain 当前覆盖率 31%, 未覆盖的调用不会被列出')
873
+ """影响分析 走工作台 MCP get_impact。"""
874
+ try:
875
+ from domain.kg.search._remote import call_remote, render_impact
876
+ data = call_remote("get_impact", {"entity": query, "depth": 2})
877
+ sys.stdout.write(render_impact(data))
878
+ except Exception as e:
879
+ print("MCP get_impact 失败: %s" % str(e)[:200])
825
880
 
826
881
 
827
882
  def search_context360(query):
828
- """符号的360度视图: 端点/函数/处理器/PRD (U1: 内存图遍历)"""
829
- from domain.kg.graph.graph_traverse import CodeGraph
830
- g = CodeGraph(INDEX_DIR)
831
- result = g.context360(query)
832
- if result:
833
- print(result)
834
- else:
835
- print('未找到匹配 "{}" 的符号。'.format(query))
883
+ """360度视图 走工作台 MCP context_360。"""
884
+ try:
885
+ from domain.kg.search._remote import call_remote, render_context360
886
+ data = call_remote("context_360", {"symbol": query})
887
+ sys.stdout.write(render_context360(data))
888
+ except Exception as e:
889
+ print("MCP context_360 失败: %s" % str(e)[:200])
836
890
 
837
891
 
838
892
  def print_usage():
@@ -29,6 +29,7 @@ Schema:
29
29
  import json
30
30
  import os
31
31
  import sys
32
+ import time
32
33
 
33
34
  import duckdb
34
35
 
@@ -102,6 +103,19 @@ def init_schema(con):
102
103
  value TEXT
103
104
  )
104
105
  """)
106
+ # ── 文件签名表 (P0: fingerprint 三级增量) ──
107
+ # 记录每个被索引文件的 content_hash (全文) 和 structural_hash (去注释去空白)。
108
+ # 增量构建前比对: NONE=完全没变; COSMETIC=只改注释/空白(跳过重算);
109
+ # STRUCTURAL=真改结构(正常重算)。图谱只记拓扑不记实现 → COSMETIC 跳过 100% 安全。
110
+ con.execute("""
111
+ CREATE TABLE IF NOT EXISTS file_signatures (
112
+ file_path TEXT PRIMARY KEY,
113
+ content_hash TEXT,
114
+ structural_hash TEXT,
115
+ level TEXT,
116
+ updated_at TEXT
117
+ )
118
+ """)
105
119
  # ── 搜索层表 ──
106
120
  con.execute("CREATE TABLE IF NOT EXISTS keywords(keyword TEXT, file_path TEXT)")
107
121
  con.execute("CREATE TABLE IF NOT EXISTS endpoints(endpoint TEXT PRIMARY KEY, controller_file TEXT)")
@@ -182,6 +196,8 @@ def init_schema(con):
182
196
  con.execute("CREATE INDEX IF NOT EXISTS idx_alias ON aliases(alias)")
183
197
  con.execute("CREATE INDEX IF NOT EXISTS idx_edge_from ON edges(from_id)")
184
198
  con.execute("CREATE INDEX IF NOT EXISTS idx_edge_to ON edges(to_id)")
199
+ # P1: 复合索引加速按类型的边查询 (calls/imports 边量大, multi_hop 按类型过滤)
200
+ con.execute("CREATE INDEX IF NOT EXISTS idx_edge_from_type ON edges(from_id, edge_type)")
185
201
  con.execute("CREATE INDEX IF NOT EXISTS idx_kw ON keywords(keyword)")
186
202
  con.execute("CREATE INDEX IF NOT EXISTS idx_page_type ON pages(page_type)")
187
203
  con.execute("CREATE INDEX IF NOT EXISTS idx_field ON fields(field)")
@@ -192,6 +208,7 @@ def init_schema(con):
192
208
  con.execute("CREATE INDEX IF NOT EXISTS idx_learn_dev ON learning_events(dev)")
193
209
  con.execute("CREATE INDEX IF NOT EXISTS idx_learn_pat_cat ON learning_patterns(category)")
194
210
  con.execute("CREATE INDEX IF NOT EXISTS idx_wf_mod ON workflows(module)")
211
+ con.execute("CREATE INDEX IF NOT EXISTS idx_fsig ON file_signatures(file_path)")
195
212
 
196
213
 
197
214
  def _sql_escape(val):
@@ -244,6 +261,32 @@ def delete_edges_from(con, from_id, edge_type=None):
244
261
  con.execute("DELETE FROM edges WHERE from_id = ?", [from_id])
245
262
 
246
263
 
264
+ def upsert_file_signature(con, file_path, content_hash, structural_hash, level):
265
+ """Insert or replace a file's signature (P0: fingerprint incremental).
266
+
267
+ Called after classifying a changed file; stored so the next incremental
268
+ run can compare and skip COSMETIC-only changes.
269
+ """
270
+ con.execute(
271
+ "INSERT OR REPLACE INTO file_signatures VALUES (?, ?, ?, ?, ?)",
272
+ [file_path, content_hash, structural_hash, level, str(int(time.time()))]
273
+ )
274
+
275
+
276
+ def get_file_signature(con, file_path):
277
+ """Read a file's stored signature, or None if never seen."""
278
+ row = con.execute(
279
+ "SELECT content_hash, structural_hash FROM file_signatures WHERE file_path = ?",
280
+ [file_path]
281
+ ).fetchone()
282
+ return {'content_hash': row[0], 'structural_hash': row[1]} if row else None
283
+
284
+
285
+ def clear_file_signatures(con):
286
+ """Wipe all signatures (full rebuild resets the baseline)."""
287
+ con.execute("DELETE FROM file_signatures")
288
+
289
+
247
290
  def resolve_entity(con, alias):
248
291
  """Resolve any alias to its canonical entity ID."""
249
292
  row = con.execute(