@hupan56/wlkj 3.0.0 → 3.1.3

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 (247) hide show
  1. package/bin/cli.js +1435 -1373
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +6 -4
  4. package/templates/qoder/commands/optional/wl-report.md +20 -39
  5. package/templates/qoder/commands/optional/wl-status.md +4 -4
  6. package/templates/qoder/commands/wl-code.md +2 -2
  7. package/templates/qoder/commands/wl-design.md +5 -5
  8. package/templates/qoder/commands/wl-init.md +3 -3
  9. package/templates/qoder/commands/wl-prd.md +26 -13
  10. package/templates/qoder/commands/wl-req.md +3 -3
  11. package/templates/qoder/commands/wl-search.md +78 -47
  12. package/templates/qoder/commands/wl-task.md +343 -40
  13. package/templates/qoder/commands/wl-test.md +10 -10
  14. package/templates/qoder/config.yaml +35 -13
  15. package/templates/qoder/hooks/post-tool-use.py +181 -0
  16. package/templates/qoder/hooks/session-start.py +82 -1
  17. package/templates/qoder/hooks/stop-eval.py +207 -0
  18. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  19. package/templates/qoder/rules/wl-pipeline.md +70 -8
  20. package/templates/qoder/scripts/README.md +78 -115
  21. package/templates/qoder/scripts/capability/__init__.py +26 -0
  22. package/templates/qoder/scripts/capability/__main__.py +72 -0
  23. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  24. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  25. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  26. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  27. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  28. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  29. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  30. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  31. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  32. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  33. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  34. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  35. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  36. package/templates/qoder/scripts/capability/chain.py +92 -0
  37. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  38. package/templates/qoder/scripts/capability/registry.py +246 -0
  39. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  40. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  42. package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
  43. package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
  44. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  45. package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +55 -23
  46. package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
  47. package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
  48. package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
  49. package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +35 -12
  50. package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
  51. package/templates/qoder/scripts/domain/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  53. package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
  54. package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
  55. package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
  56. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  57. package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
  58. package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
  59. package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
  60. package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
  61. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
  62. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
  63. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
  64. package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
  65. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  66. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
  68. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  69. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  70. package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
  71. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  72. package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
  73. package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
  74. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  75. package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
  76. package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
  77. package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
  78. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  79. package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
  80. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  81. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  82. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  83. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  84. package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
  85. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  86. package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
  87. package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
  88. package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
  89. package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
  90. package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
  91. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  92. package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
  93. package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
  94. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  95. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  96. package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
  97. package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
  98. package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
  99. package/templates/qoder/scripts/domain/task/__pycache__/syncgate.cpython-39.pyc +0 -0
  100. package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
  101. package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
  102. package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
  103. package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
  104. package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
  105. package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
  106. package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
  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/__pycache__/__init__.cpython-39.pyc +0 -0
  110. package/templates/qoder/scripts/foundation/__pycache__/bootstrap.cpython-39.pyc +0 -0
  111. package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
  112. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  113. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  114. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  115. package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
  116. package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
  117. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
  118. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  119. package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
  120. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  121. package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
  122. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
  123. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  124. package/templates/qoder/scripts/foundation/identity/__pycache__/__init__.cpython-39.pyc +0 -0
  125. package/templates/qoder/scripts/foundation/identity/__pycache__/identity.cpython-39.pyc +0 -0
  126. package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
  127. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
  128. package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
  129. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
  130. package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
  131. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  132. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  133. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
  134. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  135. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  136. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  137. package/templates/qoder/scripts/foundation/io/__pycache__/__init__.cpython-39.pyc +0 -0
  138. package/templates/qoder/scripts/foundation/io/__pycache__/atomicio.cpython-39.pyc +0 -0
  139. package/templates/qoder/scripts/foundation/io/__pycache__/filelock.cpython-39.pyc +0 -0
  140. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  141. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  142. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  144. package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
  145. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  146. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  147. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  148. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  149. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  150. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  151. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  152. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  153. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  154. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  155. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  156. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  157. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  158. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  159. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  160. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  161. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  162. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  163. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  164. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  165. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  166. package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
  167. package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
  168. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  169. package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
  170. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  171. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  172. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  173. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  174. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  175. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  176. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  177. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  178. package/templates/qoder/scripts/validation/__init__.py +0 -0
  179. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  180. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  181. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  182. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  183. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  184. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  185. package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
  186. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  187. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  188. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  189. package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
  190. package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
  191. package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
  192. package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
  193. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  194. package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
  195. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  196. package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
  197. package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
  198. package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
  199. package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
  200. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  201. package/templates/qoder/settings.json +30 -0
  202. package/templates/qoder/skills/design-import/SKILL.md +22 -13
  203. package/templates/qoder/skills/design-review/SKILL.md +19 -10
  204. package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
  205. package/templates/qoder/skills/prd-review/SKILL.md +6 -4
  206. package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
  207. package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
  208. package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
  209. package/templates/qoder/skills/test-generator/SKILL.md +7 -5
  210. package/templates/qoder/skills/wl-code/SKILL.md +23 -11
  211. package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
  212. package/templates/qoder/skills/wl-design/SKILL.md +5 -3
  213. package/templates/qoder/skills/wl-init/SKILL.md +6 -6
  214. package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
  215. package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
  216. package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
  217. package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
  218. package/templates/qoder/skills/wl-report/SKILL.md +25 -23
  219. package/templates/qoder/skills/wl-search/SKILL.md +149 -78
  220. package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
  221. package/templates/qoder/skills/wl-status/SKILL.md +48 -16
  222. package/templates/qoder/skills/wl-task/SKILL.md +92 -10
  223. package/templates/qoder/skills/wl-test/SKILL.md +75 -29
  224. package/templates/qoder/templates/prd-full-template.md +7 -0
  225. package/templates/root/AGENTS.md +275 -236
  226. package/templates/root/requirements.txt +3 -0
  227. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  228. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  229. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
  230. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
  231. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
  232. package/templates/.qoder/learning/eval-history.jsonl +0 -28
  233. package/templates/data/index/wiki-index.json +0 -8
  234. package/templates/qoder/nul +0 -4
  235. package/templates/qoder/scripts/kg/extract.py +0 -398
  236. package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
  237. package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
  238. package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
  239. package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
  240. package/templates/qoder/scripts/task/__init__.py +0 -11
  241. package/templates/qoder/scripts/test/__init__.py +0 -11
  242. package/templates/qoder/scripts/wlkj.py +0 -116
  243. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  244. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  245. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  246. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  247. /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ nexus MCP 端到端 smoke test。
5
+
6
+ 用法:
7
+ python .qoder/scripts/capability/smoke_test.py
8
+
9
+ 验证:
10
+ 1. McpRegistry 能 spawn 每个 MCP server 子进程
11
+ 2. list_tools 能拿到真实工具清单
12
+ 3. call_tool 能端到端调通(以 kg 的 search_code 为例)
13
+ 4. Memory 职责链链头是否可用
14
+
15
+ 输出: 每个 server 的 PASS/FAIL + tools_indexed + latency_ms。
16
+ 退出码: 0=全 PASS,1=有 FAIL。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+
25
+ # 确保 scripts/ 在 sys.path
26
+ _here = os.path.dirname(os.path.abspath(__file__))
27
+ _scripts = os.path.dirname(_here)
28
+ if _scripts not in sys.path:
29
+ sys.path.insert(0, _scripts)
30
+
31
+ # UTF-8 (Windows)
32
+ try:
33
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
34
+ except Exception:
35
+ pass
36
+
37
+
38
+ def _section(title: str) -> None:
39
+ print("\n" + "=" * 60)
40
+ print(f" {title}")
41
+ print("=" * 60)
42
+
43
+
44
+ def test_registry() -> dict:
45
+ """测试 McpRegistry: 每个 server 的 list_tools + call_tool。"""
46
+ from capability.registry_mcp import McpRegistry
47
+
48
+ reg = McpRegistry()
49
+ results = {}
50
+ _section("McpRegistry server 探活")
51
+
52
+ for server_name in reg._servers:
53
+ t0 = time.monotonic()
54
+ try:
55
+ transport = reg._get_or_create(server_name)
56
+ available = transport.available
57
+ tools = transport.list_tools() if available else []
58
+ latency_ms = (time.monotonic() - t0) * 1000
59
+ results[server_name] = {
60
+ "available": available,
61
+ "tools_indexed": len(tools),
62
+ "latency_ms": round(latency_ms, 1),
63
+ "sample_tools": [t.get("name", "") for t in tools[:5]],
64
+ "spawn_pid": getattr(getattr(transport, "_proc", None), "pid", None),
65
+ "status": "PASS" if available and len(tools) > 0 else "FAIL",
66
+ }
67
+ except Exception as e:
68
+ latency_ms = (time.monotonic() - t0) * 1000
69
+ results[server_name] = {
70
+ "available": False,
71
+ "tools_indexed": 0,
72
+ "latency_ms": round(latency_ms, 1),
73
+ "error": str(e)[:120],
74
+ "status": "FAIL",
75
+ }
76
+
77
+ for name, r in results.items():
78
+ icon = "✅" if r["status"] == "PASS" else "❌"
79
+ pid = f" pid={r['spawn_pid']}" if r.get("spawn_pid") else ""
80
+ print(f" {icon} {name}: tools={r['tools_indexed']} "
81
+ f"latency={r['latency_ms']}ms{pid} sample={r.get('sample_tools', [])[:3]}")
82
+ if r.get("error"):
83
+ print(f" error: {r['error']}")
84
+
85
+ return results
86
+
87
+
88
+ def test_call_kg_search(reg) -> dict:
89
+ """端到端调用: kg search_code(验证 call_tool 链路)。"""
90
+ _section("端到端 call_tool: search_code")
91
+ t0 = time.monotonic()
92
+ try:
93
+ result = reg.call("search_code", {"keyword": "保险"})
94
+ latency_ms = (time.monotonic() - t0) * 1000
95
+ ok = not result.is_error and len(result.text) > 0
96
+ return {
97
+ "status": "PASS" if ok else "FAIL",
98
+ "latency_ms": round(latency_ms, 1),
99
+ "text_preview": result.text[:200] if result.text else "",
100
+ "is_error": result.is_error,
101
+ }
102
+ except Exception as e:
103
+ latency_ms = (time.monotonic() - t0) * 1000
104
+ return {
105
+ "status": "FAIL",
106
+ "latency_ms": round(latency_ms, 1),
107
+ "error": str(e)[:120],
108
+ }
109
+
110
+
111
+ def test_memory_chain() -> dict:
112
+ """测试 memory 职责链链头是否可用。"""
113
+ _section("Memory 职责链")
114
+ from capability.registry import resolve
115
+
116
+ cap = resolve()
117
+ mem = cap.memory
118
+ has_providers = hasattr(mem, "providers_status")
119
+ available = getattr(mem, "available", False)
120
+ providers = mem.providers_status() if has_providers else []
121
+
122
+ result = {
123
+ "available": available,
124
+ "providers": providers,
125
+ "status": "PASS" if available else "WARN",
126
+ }
127
+
128
+ icon = "✅" if available else "⚠️"
129
+ print(f" {icon} memory.available={available}")
130
+ for p in providers:
131
+ hit = " ← ACTIVE" if p.get("hit") else ""
132
+ print(f" {p['provider']}: available={p['available']}{hit}")
133
+
134
+ return result
135
+
136
+
137
+ def main() -> int:
138
+ print("nexus MCP smoke test")
139
+ print(f"时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
140
+ print(f"Python: {sys.executable}")
141
+ print(f"CWD: {os.getcwd()}")
142
+
143
+ # 1. Registry 探活
144
+ from capability.registry_mcp import McpRegistry
145
+ reg = McpRegistry()
146
+ server_results = test_registry()
147
+
148
+ # 2. 端到端 call_tool(只在 kg 可用时跑)
149
+ call_result = {}
150
+ kg_ok = server_results.get("qoder-knowledge-graph", {}).get("status") == "PASS"
151
+ if kg_ok:
152
+ call_result = test_call_kg_search(reg)
153
+ icon = "✅" if call_result["status"] == "PASS" else "❌"
154
+ print(f"\n {icon} search_code: latency={call_result['latency_ms']}ms "
155
+ f"text_len={len(call_result.get('text_preview', ''))}")
156
+ if call_result.get("error"):
157
+ print(f" error: {call_result['error']}")
158
+ else:
159
+ print("\n ⏭️ kg server 不可用,跳过 call_tool 测试")
160
+
161
+ # 3. Memory 链
162
+ memory_result = test_memory_chain()
163
+
164
+ # 4. 汇总
165
+ _section("汇总")
166
+ all_pass = all(r.get("status") == "PASS" for r in server_results.values())
167
+ call_pass = call_result.get("status") == "PASS" or not kg_ok
168
+ mem_pass = memory_result.get("status") in ("PASS", "WARN")
169
+ total_pass = all_pass and call_pass and mem_pass
170
+
171
+ total_tools = sum(r.get("tools_indexed", 0) for r in server_results.values())
172
+ active_servers = sum(1 for r in server_results.values() if r.get("status") == "PASS")
173
+
174
+ print(f" Servers: {active_servers}/{len(server_results)} PASS")
175
+ print(f" Tools indexed: {total_tools}")
176
+ print(f" Call tool: {call_result.get('status', 'SKIP')}")
177
+ print(f" Memory chain: {memory_result.get('status', 'SKIP')}")
178
+ print(f"\n {'✅ ALL PASS' if total_pass else '❌ SOME FAILED'}")
179
+
180
+ # 写 JSON 报告(可选)
181
+ report = {
182
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
183
+ "servers": server_results,
184
+ "call_tool": call_result,
185
+ "memory": memory_result,
186
+ "summary": {
187
+ "all_pass": total_pass,
188
+ "active_servers": active_servers,
189
+ "total_servers": len(server_results),
190
+ "total_tools": total_tools,
191
+ },
192
+ }
193
+ report_path = os.path.join(_here, "smoke_test_report.json")
194
+ try:
195
+ with open(report_path, "w", encoding="utf-8") as f:
196
+ json.dump(report, f, ensure_ascii=False, indent=2)
197
+ print(f"\n 报告已写入: {report_path}")
198
+ except Exception:
199
+ pass
200
+
201
+ # 关闭所有 transport
202
+ try:
203
+ reg.close_all()
204
+ except Exception:
205
+ pass
206
+
207
+ return 0 if total_pass else 1
208
+
209
+
210
+ if __name__ == "__main__":
211
+ sys.exit(main())
@@ -0,0 +1,94 @@
1
+ {
2
+ "timestamp": "2026-06-26 21:25:55",
3
+ "servers": {
4
+ "qoder-knowledge-graph": {
5
+ "available": true,
6
+ "tools_indexed": 17,
7
+ "latency_ms": 437.0,
8
+ "sample_tools": [
9
+ "search_code",
10
+ "search_api",
11
+ "search_prd",
12
+ "get_impact",
13
+ "context_360"
14
+ ],
15
+ "spawn_pid": 38616,
16
+ "status": "PASS"
17
+ },
18
+ "qoder-zentao": {
19
+ "available": true,
20
+ "tools_indexed": 66,
21
+ "latency_ms": 3657.0,
22
+ "sample_tools": [
23
+ "check_zentao_status",
24
+ "get_zentao_url",
25
+ "list_products",
26
+ "get_product",
27
+ "create_product"
28
+ ],
29
+ "spawn_pid": 11224,
30
+ "status": "PASS"
31
+ },
32
+ "qoder-mysql": {
33
+ "available": true,
34
+ "tools_indexed": 6,
35
+ "latency_ms": 3484.0,
36
+ "sample_tools": [
37
+ "list_envs",
38
+ "query_schema",
39
+ "query_data",
40
+ "query_distinct",
41
+ "list_databases"
42
+ ],
43
+ "spawn_pid": 37868,
44
+ "status": "PASS"
45
+ },
46
+ "lanhu": {
47
+ "available": true,
48
+ "tools_indexed": 13,
49
+ "latency_ms": 3156.0,
50
+ "sample_tools": [
51
+ "lanhu_resolve_invite_link",
52
+ "lanhu_list_product_documents",
53
+ "lanhu_get_pages",
54
+ "lanhu_get_ai_analyze_page_result",
55
+ "lanhu_get_designs"
56
+ ],
57
+ "spawn_pid": 36836,
58
+ "status": "PASS"
59
+ }
60
+ },
61
+ "call_tool": {
62
+ "status": "PASS",
63
+ "latency_ms": 3953.0,
64
+ "text_preview": "Chinese: 保险 -> English: ins insurance vehlife/insurance\n\n[Carmg-H5] 12 files\n Carmg-H5/src/.../main/List.vue -> data/code/Carmg-H5/src/modules/icsAbnormalCenter/module/main/List.vue\n Carmg-H5/src/",
65
+ "is_error": false
66
+ },
67
+ "memory": {
68
+ "available": true,
69
+ "providers": [
70
+ {
71
+ "provider": "McpMemoryProvider",
72
+ "available": true,
73
+ "hit": true
74
+ },
75
+ {
76
+ "provider": "FileMemoryCap",
77
+ "available": true,
78
+ "hit": false
79
+ },
80
+ {
81
+ "provider": "NoOpMemoryCap",
82
+ "available": false,
83
+ "hit": false
84
+ }
85
+ ],
86
+ "status": "PASS"
87
+ },
88
+ "summary": {
89
+ "all_pass": true,
90
+ "active_servers": 4,
91
+ "total_servers": 4,
92
+ "total_tools": 102
93
+ }
94
+ }
@@ -1,11 +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)
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)
@@ -13,7 +13,12 @@ v3.0 合并自: check_carriers + check_qoderwork_consistency + sync_carriers。
13
13
  """
14
14
  # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
15
15
  import os as _o, sys as _s
16
- _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
16
+ _f = _o.path.abspath(__file__)
17
+ for _ in range(10):
18
+ _f = _o.path.dirname(_f)
19
+ _cp = _o.path.join(_f, 'foundation')
20
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
21
+ break
17
22
  if _cp not in _s.path: _s.path.insert(0, _cp)
18
23
  from bootstrap import setup; setup()
19
24
 
@@ -22,20 +27,15 @@ try:
22
27
  import yaml
23
28
  except ImportError:
24
29
  yaml=None
25
- from common.paths import get_repo_root
26
- BASE = str(get_repo_root())
27
- COMMANDS_DIR = os.path.join(BASE, '.qoder', 'commands')
28
- SKILLS_DIR = os.path.join(BASE, '.qoder', 'skills')
29
- AGENTS_MD = os.path.join(BASE, 'AGENTS.md')
30
-
31
- # ==================== 来自 check_carriers.py ====================
32
- from common.paths import get_repo_root
30
+ from foundation.core.paths import get_repo_root
33
31
 
34
32
  BASE = get_repo_root()
35
33
  COMMANDS_DIR = BASE / '.qoder' / 'commands'
36
34
  SKILLS_DIR = BASE / '.qoder' / 'skills'
37
35
  AGENTS_MD = BASE / 'AGENTS.md'
38
36
 
37
+ # ==================== 来自 check_carriers.py ====================
38
+
39
39
  # description 相似度阈值 (改了标点/措辞不算漂移, 实质改变才算)
40
40
  DESC_SIMILAR_THRESHOLD = 0.6
41
41
 
@@ -234,7 +234,7 @@ def main_check_carriers():
234
234
 
235
235
 
236
236
  # ==================== 来自 check_qoderwork_consistency.py ====================
237
- from common.paths import get_repo_root # noqa: E402
237
+ # (复用顶部已定义的 get_repo_root / BASE / COMMANDS_DIR / SKILLS_DIR)
238
238
 
239
239
  # 扫描目标: commands 全部 .md + skills 的 SKILL.md
240
240
  SCAN_GLOBS = [
@@ -246,6 +246,10 @@ SCAN_GLOBS = [
246
246
  # 匹配 `python .qoder/scripts/` 或 `python3 .qoder/scripts/`, 不带 $R 引号包裹
247
247
  BARE_PAT = re.compile(r'(?<![\"\$])\bpython3?\s+\.qoder/scripts/([A-Za-z0-9_./-]+)')
248
248
 
249
+ # 叶子路径 (能抓 $R 前缀和裸路径两种): .qoder/scripts/<leaf>.py
250
+ # 用于"存在性校验" — 本次断链的根因: 叶子路径文件不存在, 但旧检查器只查 $R 前缀没查存在性
251
+ LEAF_PAT = re.compile(r'\.qoder/scripts/([A-Za-z0-9_./-]+\.py)')
252
+
249
253
  # 已安全模式: cd "$R" && python .qoder/scripts/...
250
254
  SAFE_CD_PAT = re.compile(r'cd\s+["\']?\$R["\']?\s*&&\s*python3?\s+\.qoder/scripts/')
251
255
 
@@ -256,12 +260,13 @@ ROOT_LOCATOR_PAT = re.compile(r'repo_root\.py|R=\$\(python.*repo_root')
256
260
  HINT_CTX = 'install_qoderwork.py'
257
261
 
258
262
 
259
- def scan_file(path, strict):
263
+ def scan_file(path, strict, repo):
260
264
  """扫一个文件, 返回 (errors, warnings, root_block_present)。
261
- errors = 裸相对路径真实调用(必修); warnings = 无 R 块等(建议修)。
265
+ errors = 断链(叶子路径文件不存在) 或 裸相对路径(QoderWork 失效); warnings = 无 R 块等。
262
266
  """
263
267
  errors = []
264
268
  warnings = []
269
+ seen_leaves = set() # (行号, leaf) 去重, 避免裸路径+不存在双重报
265
270
  try:
266
271
  with open(path, encoding='utf-8') as f:
267
272
  lines = f.readlines()
@@ -277,12 +282,19 @@ def scan_file(path, strict):
277
282
  # 豁免: 已安全的 cd "$R" && python 模式
278
283
  if SAFE_CD_PAT.search(line):
279
284
  continue
280
- # 找裸相对路径
285
+ # 1. 裸相对路径风格 (QoderWork 桌面端工作目录非仓库根, 裸路径会失效)
281
286
  for m in BARE_PAT.finditer(line):
282
287
  script = m.group(1)
283
- # 过滤掉明显是文字描述(行里既无 bash 代码块上下文也无命令感)
284
- # 简化: 只要匹配到就算(宁可误报让 reviewer 看一眼)
285
- errors.append((i, line.rstrip(), script))
288
+ seen_leaves.add((i, script))
289
+ errors.append((i, line.rstrip(), script + ' (裸相对路径)'))
290
+ # 2. 叶子路径存在性校验 — 本次断链根因 (短子包路径文件不存在但旧检查器没校验)
291
+ for m in LEAF_PAT.finditer(line):
292
+ leaf = m.group(1)
293
+ if (i, leaf) in seen_leaves:
294
+ continue
295
+ seen_leaves.add((i, leaf))
296
+ if not (repo / '.qoder' / 'scripts' / leaf).is_file():
297
+ errors.append((i, line.rstrip(), '.qoder/scripts/' + leaf + ' (文件不存在)'))
286
298
 
287
299
  # 无 R 块但文件里有 $R 引用 → 警告
288
300
  if not root_block and '$R' in ''.join(lines):
@@ -312,7 +324,7 @@ def main_check_paths():
312
324
  if not fnmatch.fnmatch(fn, pattern):
313
325
  continue
314
326
  path = os.path.join(dirpath, fn)
315
- errors, warnings, root_block = scan_file(path, args.strict)
327
+ errors, warnings, root_block = scan_file(path, args.strict, repo)
316
328
  rel = os.path.relpath(path, str(repo))
317
329
  if errors:
318
330
  files_with_errors.append((rel, errors))
@@ -364,11 +376,7 @@ def main_check_paths():
364
376
 
365
377
 
366
378
  # ==================== 来自 sync_carriers.py ====================
367
- from common.paths import get_repo_root
368
-
369
- BASE = get_repo_root()
370
- COMMANDS_DIR = BASE / '.qoder' / 'commands'
371
- SKILLS_DIR = BASE / '.qoder' / 'skills'
379
+ # (复用顶部已定义的 get_repo_root / BASE / COMMANDS_DIR / SKILLS_DIR)
372
380
 
373
381
 
374
382
  def _read_frontmatter(path):
@@ -597,8 +605,7 @@ def main_all():
597
605
  改了规则/技能后跑一次, 4载体全同步。
598
606
  """
599
607
  import subprocess, os
600
- SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
601
- BASE = os.path.dirname(os.path.dirname(SCRIPTS))
608
+ # 复用顶部 BASE (= 仓库根) 和 SCRIPTS_DIR; 避免再次手算路径
602
609
  print('=' * 56)
603
610
  print('全系列同步 (4载体: IDE/Quest/CLI/QoderWork)')
604
611
  print('=' * 56)
@@ -613,7 +620,7 @@ def main_all():
613
620
 
614
621
  # 2. sync_templates (npm 镜像)
615
622
  print('\n[2/3] 同步 npm 镜像 (.qoder→packages/wlkj/templates)...')
616
- sync_script = os.path.join(BASE, 'packages', 'wlkj', 'sync_templates.py')
623
+ sync_script = str(BASE / 'packages' / 'wlkj' / 'sync_templates.py')
617
624
  if os.path.isfile(sync_script):
618
625
  r = subprocess.run([sys.executable, sync_script], capture_output=True, text=True, encoding='utf-8')
619
626
  print(r.stdout.strip().split('\n')[-1] if r.stdout else ' (无输出)')
@@ -622,7 +629,7 @@ def main_all():
622
629
 
623
630
  # 3. install_qoderwork (投影 ~/.qoderwork/)
624
631
  print('\n[3/3] 投影 QoderWork (skills/commands→~/.qoderwork/)...')
625
- install_script = os.path.join(SCRIPTS, 'setup', 'install_qoderwork.py')
632
+ install_script = str(BASE / '.qoder' / 'scripts' / 'deployment' / 'setup' / 'install_qoderwork.py')
626
633
  if os.path.isfile(install_script):
627
634
  r = subprocess.run([sys.executable, install_script, '--force-commands'],
628
635
  capture_output=True, text=True, encoding='utf-8', timeout=60)
@@ -0,0 +1,148 @@
1
+ import os
2
+ import sys
3
+
4
+ HERE = os.path.dirname(os.path.abspath(__file__))
5
+ SCRIPTS = None
6
+ cur = HERE
7
+ for _ in range(8):
8
+ if os.path.isdir(os.path.join(cur, "foundation")) and os.path.isdir(os.path.join(cur, "domain")):
9
+ SCRIPTS = cur
10
+ break
11
+ cur = os.path.dirname(cur)
12
+ if SCRIPTS is None:
13
+ SCRIPTS = os.path.dirname(os.path.dirname(HERE))
14
+ if SCRIPTS not in sys.path:
15
+ sys.path.insert(0, SCRIPTS)
16
+ sys.path.insert(0, os.path.join(SCRIPTS, "foundation"))
17
+ try:
18
+ from bootstrap import setup
19
+ setup()
20
+ except Exception as e:
21
+ print("bootstrap setup failed: %s" % e)
22
+
23
+ from foundation.core.paths import get_repo_root
24
+ ROOT = get_repo_root()
25
+
26
+ COMMANDS = os.path.join(str(ROOT), ".qoder", "commands")
27
+ SKILLS = os.path.join(str(ROOT), ".qoder", "skills")
28
+ AGENTS = os.path.join(str(ROOT), "AGENTS.md")
29
+
30
+ CMD_PREFIX = "wl-"
31
+
32
+ def names_cmds():
33
+ s = set()
34
+ if os.path.isdir(COMMANDS):
35
+ for r, d, fs in os.walk(COMMANDS):
36
+ for f in fs:
37
+ if f.endswith(".md") and f.startswith(CMD_PREFIX):
38
+ s.add(os.path.splitext(f)[0])
39
+ return s
40
+
41
+ def names_skills():
42
+ s = set()
43
+ if os.path.isdir(SKILLS):
44
+ for d in os.listdir(SKILLS):
45
+ p = os.path.join(SKILLS, d, "SKILL.md")
46
+ if os.path.isfile(p) and d.startswith(CMD_PREFIX):
47
+ s.add(d)
48
+ return s
49
+
50
+ def agents_table_cmds():
51
+ s = set()
52
+ if not os.path.isfile(AGENTS):
53
+ return s, False
54
+ txt = open(AGENTS, encoding="utf-8", errors="ignore").read()
55
+ in_table = False
56
+ for line in txt.splitlines():
57
+ if "7 Core" in line or "核心命令" in line:
58
+ in_table = True
59
+ if not in_table:
60
+ continue
61
+ # match /wl-xxx only when followed by a table cell boundary
62
+ i = 0
63
+ while True:
64
+ a = line.find("/wl-", i)
65
+ if a < 0:
66
+ break
67
+ b = a + 4
68
+ while b < len(line) and (line[b].isalpha() or line[b] == "-"):
69
+ b += 1
70
+ # name must be at least /wl- + 1 char, and next char must be a boundary
71
+ ok = b >= a + 5
72
+ nxt = line[b] if b < len(line) else ""
73
+ if ok and nxt in ("`", " ", "|", "", "\t"):
74
+ s.add(line[a + 1:b])
75
+ i = b
76
+ return s, True
77
+
78
+ def main():
79
+ errs = 0
80
+ warns = 0
81
+ print("=" * 60)
82
+ print("carriers verify: 4-carrier name-set + table integrity")
83
+ print("ROOT = %s" % ROOT)
84
+ print("=" * 60)
85
+
86
+ cs = names_cmds()
87
+ ss = names_skills()
88
+ ts, has_agents = agents_table_cmds()
89
+
90
+ only_cmd = sorted(cs - ss)
91
+ only_skill = sorted(ss - cs)
92
+
93
+ print("\n[1] command<->skill name set diff (wl-* prefixed only)")
94
+ print(" commands: %d, skills: %d" % (len(cs), len(ss)))
95
+ if only_cmd:
96
+ warns += 1
97
+ print(" [warn] commands without matching skill dir:")
98
+ for n in only_cmd:
99
+ print(" -%s" % n)
100
+ # 白名单: wl-prd-full/quick/review 是 wl-prd 路由器命令的模式子 skill (设计如此, 非漂移)
101
+ PRD_ROUTER_SKILLS = {"wl-prd-full", "wl-prd-quick", "wl-prd-review"}
102
+ real_orphans = [n for n in only_skill if n not in PRD_ROUTER_SKILLS]
103
+ prd_router = [n for n in only_skill if n in PRD_ROUTER_SKILLS]
104
+ if real_orphans:
105
+ errs += 1
106
+ print(" [FAIL] skills without matching command file (orphan):")
107
+ for n in real_orphans:
108
+ print(" -%s" % n)
109
+ if prd_router:
110
+ print(" [info] %d prd-router sub-skills (wl-prd-full/quick/review, by-design)" % len(prd_router))
111
+ if not only_cmd and not only_skill:
112
+ print(" [ok] wl-* command<->skill name sets aligned")
113
+
114
+ print("\n[2] AGENTS.md core-command table vs files")
115
+ if not has_agents:
116
+ errs += 1
117
+ print(" [FAIL] AGENTS.md missing")
118
+ else:
119
+ missing = sorted(ts - cs - ss)
120
+ if missing:
121
+ errs += 1
122
+ print(" [FAIL] table lists commands that have no file:")
123
+ for n in missing:
124
+ print(" /%s" % n)
125
+ else:
126
+ print(" [ok] all %d table entries have a file" % len(ts))
127
+
128
+ print("\n[3] delegate content check to existing carriers.py check")
129
+ chk = os.path.join(str(ROOT), ".qoder", "scripts", "deployment", "setup", "carriers.py")
130
+ if os.path.isfile(chk):
131
+ import subprocess
132
+ r = subprocess.run([sys.executable, chk, "check"], capture_output=True, text=True, encoding="utf-8", errors="replace")
133
+ if r.returncode == 0:
134
+ print(" [ok] carriers check passed")
135
+ else:
136
+ errs += 1
137
+ print(" [FAIL] carriers check reported drift (rc=%d)" % r.returncode)
138
+ for ln in (r.stdout or "").splitlines():
139
+ if ln.strip():
140
+ print(" %s" % ln[:90])
141
+
142
+ print("-" * 60)
143
+ print("RESULT: %d FAIL / %d warn" % (errs, warns))
144
+ print("FAIL=must-fix drift; warn=advisory. exit 0 only if FAIL==0.")
145
+ return 1 if errs else 0
146
+
147
+ if __name__ == "__main__":
148
+ sys.exit(main())