@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.
- package/bin/cli.js +1435 -1107
- package/package.json +29 -29
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +161 -161
- package/templates/qoder/commands/optional/wl-report.md +20 -39
- package/templates/qoder/commands/optional/wl-status.md +4 -4
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +6 -6
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +26 -13
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +78 -47
- package/templates/qoder/commands/wl-task.md +343 -40
- package/templates/qoder/commands/wl-test.md +43 -17
- package/templates/qoder/config.yaml +52 -14
- package/templates/qoder/hooks/post-tool-use.py +181 -0
- package/templates/qoder/hooks/session-start.py +94 -23
- package/templates/qoder/hooks/stop-eval.py +207 -0
- package/templates/qoder/hooks/user-prompt-submit.py +139 -0
- package/templates/qoder/rules/wl-pipeline.md +85 -49
- package/templates/qoder/scripts/README.md +102 -0
- package/templates/qoder/scripts/capability/__init__.py +26 -0
- package/templates/qoder/scripts/capability/__main__.py +72 -0
- package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
- package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
- package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
- package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
- package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
- package/templates/qoder/scripts/capability/caps/context.py +36 -0
- package/templates/qoder/scripts/capability/caps/cron.py +50 -0
- package/templates/qoder/scripts/capability/caps/identity.py +43 -0
- package/templates/qoder/scripts/capability/caps/memory.py +71 -0
- package/templates/qoder/scripts/capability/caps/notify.py +41 -0
- package/templates/qoder/scripts/capability/caps/present.py +48 -0
- package/templates/qoder/scripts/capability/caps/repo.py +31 -0
- package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
- package/templates/qoder/scripts/capability/chain.py +92 -0
- package/templates/qoder/scripts/capability/memory_chain.py +41 -0
- package/templates/qoder/scripts/capability/registry.py +246 -0
- package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
- package/templates/qoder/scripts/capability/smoke_test.py +211 -0
- package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
- package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
- package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
- package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
- package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
- package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
- package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
- package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
- package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
- package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
- package/templates/qoder/scripts/domain/design/__init__.py +0 -0
- package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
- package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
- package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
- package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
- package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
- package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
- package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
- package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
- package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
- package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
- package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
- package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
- package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
- package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
- package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
- package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
- package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
- package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
- package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
- package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
- package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
- package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
- package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
- package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
- package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
- package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
- package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
- package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
- package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
- package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
- package/templates/qoder/scripts/domain/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
- package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
- package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
- package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
- package/templates/qoder/scripts/domain/report/role.py +39 -0
- package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
- package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
- package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
- package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
- package/templates/qoder/scripts/domain/requirement/req.py +228 -0
- package/templates/qoder/scripts/domain/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
- package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
- package/templates/qoder/scripts/domain/task/task.py +229 -0
- package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
- package/templates/qoder/scripts/domain/task/task_query.py +162 -0
- package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
- package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
- package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
- package/templates/qoder/scripts/foundation/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
- package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
- package/templates/qoder/scripts/foundation/core/config.py +187 -0
- package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
- package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/data/contract.py +317 -0
- package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
- package/templates/qoder/scripts/foundation/data/result.py +223 -0
- package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
- package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
- package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
- package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
- package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
- package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
- package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
- package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
- package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
- package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
- package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
- package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
- package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
- package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
- package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
- package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
- package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
- package/templates/qoder/scripts/orchestration/__init__.py +0 -0
- package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
- package/templates/qoder/scripts/protocol/__init__.py +0 -0
- package/templates/qoder/scripts/protocol/browser/README.md +207 -0
- package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
- package/templates/qoder/scripts/protocol/browser/config.env +9 -0
- package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
- package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
- package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
- package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
- package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
- package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
- package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
- package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
- package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
- package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
- package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
- package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
- package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
- package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
- package/templates/qoder/scripts/protocol/transports/base.py +87 -0
- package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
- package/templates/qoder/scripts/protocol/transports/http.py +141 -0
- package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
- package/templates/qoder/scripts/run_weekly_update.bat +27 -18
- package/templates/qoder/scripts/run_weekly_update.sh +22 -13
- package/templates/qoder/scripts/validation/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
- package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
- package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
- package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
- package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
- package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
- package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
- package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
- package/templates/qoder/scripts/validation/test/__init__.py +11 -0
- package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
- package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
- package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
- package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
- package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
- package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
- package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
- package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
- package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
- package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
- package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
- package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
- package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
- package/templates/qoder/settings.json +23 -2
- package/templates/qoder/skills/design-import/SKILL.md +235 -226
- package/templates/qoder/skills/design-review/SKILL.md +91 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
- package/templates/qoder/skills/prd-review/SKILL.md +8 -6
- package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
- package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
- package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
- package/templates/qoder/skills/test-generator/SKILL.md +9 -7
- package/templates/qoder/skills/wl-code/SKILL.md +25 -13
- package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
- package/templates/qoder/skills/wl-design/SKILL.md +7 -5
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
- package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
- package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
- package/templates/qoder/skills/wl-report/SKILL.md +27 -25
- package/templates/qoder/skills/wl-search/SKILL.md +151 -80
- package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
- package/templates/qoder/skills/wl-status/SKILL.md +50 -18
- package/templates/qoder/skills/wl-task/SKILL.md +94 -12
- package/templates/qoder/skills/wl-test/SKILL.md +154 -45
- package/templates/qoder/templates/prd-full-template.md +7 -0
- package/templates/root/AGENTS.md +275 -237
- package/templates/root/requirements.txt +3 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp.py +0 -298
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_api_params.py +0 -246
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/kg_mcp_server.py +0 -801
- package/templates/qoder/scripts/kg_semantic.py +0 -150
- package/templates/qoder/scripts/mcp_launcher.py +0 -414
- package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
- /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
- /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
- /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
|
File without changes
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
contract.py - 契约校验器 (解耦模块的核心)
|
|
4
|
+
|
|
5
|
+
把"君子协定"变成机器可校验。每个产出 (PRD/原型/spec/design-spec/test-cases)
|
|
6
|
+
带 @contract 头, 本模块解析头部 + 按 contracts/schemas/*.schema.json 校验。
|
|
7
|
+
|
|
8
|
+
设计原则 (与方案一致):
|
|
9
|
+
- 解耦: 模块间只读契约产出, 不读对方实现。实现可任意替换。
|
|
10
|
+
- 降级而非阻塞: 校验 FAIL 只提示, 不让模块停摆 (符合 contract-header.md)
|
|
11
|
+
- 轻量: 不引 jsonschema 重依赖, 用 stdlib 实现核心校验 (类型/必填/枚举/正则)
|
|
12
|
+
- 兼容现有: 复用现有 @contract 头格式 (MD 的 HTML 注释 / JSON 的 _contract 字段)
|
|
13
|
+
|
|
14
|
+
核心 API:
|
|
15
|
+
parse_contract(path) -> (contract_dict, source_text)
|
|
16
|
+
解析文件的契约头 (MD/JSON 两种)
|
|
17
|
+
validate_contract(path) -> Result
|
|
18
|
+
解析 + 按 schema 校验, 返回统一 Result
|
|
19
|
+
extract_contract_header(md_text) -> dict | None
|
|
20
|
+
从 Markdown 正文提取 <!-- @contract ... --> 块
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
from foundation.data.contract import validate_contract
|
|
24
|
+
r = validate_contract('workspace/.../REQ-2026-001-prd.md')
|
|
25
|
+
if r.is_warn:
|
|
26
|
+
print('警告:', r.msg) # 软门禁: 带警告继续
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Dict, Optional, Tuple
|
|
35
|
+
|
|
36
|
+
# 相对导入
|
|
37
|
+
try:
|
|
38
|
+
from .result import Result, CODE
|
|
39
|
+
from foundation.io.atomicio import safe_read_json
|
|
40
|
+
except ImportError:
|
|
41
|
+
_COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
42
|
+
if _COMMON_DIR not in sys.path:
|
|
43
|
+
sys.path.insert(0, _COMMON_DIR)
|
|
44
|
+
from result import Result, CODE
|
|
45
|
+
from atomicio import safe_read_json
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"parse_contract",
|
|
49
|
+
"validate_contract",
|
|
50
|
+
"extract_contract_header",
|
|
51
|
+
"extract_json_contract",
|
|
52
|
+
"SCHEMAS_DIR",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# schemas 目录 (相对仓库根: .qoder/contracts/schemas/)
|
|
56
|
+
def _find_schemas_dir() -> Path:
|
|
57
|
+
"""定位 contracts/schemas/。向上找 .qoder/contracts/schemas/。"""
|
|
58
|
+
here = Path(__file__).resolve().parent
|
|
59
|
+
for _ in range(8):
|
|
60
|
+
cand = here / "contracts" / "schemas"
|
|
61
|
+
if cand.is_dir():
|
|
62
|
+
return cand
|
|
63
|
+
# common/ 在 .qoder/scripts/common/, schemas 在 .qoder/contracts/schemas/
|
|
64
|
+
cand2 = here.parent.parent / "contracts" / "schemas"
|
|
65
|
+
if cand2.is_dir():
|
|
66
|
+
return cand2
|
|
67
|
+
parent = here.parent
|
|
68
|
+
if parent == here:
|
|
69
|
+
break
|
|
70
|
+
here = parent
|
|
71
|
+
return Path(".qoder/contracts/schemas")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
SCHEMAS_DIR = _find_schemas_dir()
|
|
75
|
+
|
|
76
|
+
# 契约类型 → schema 文件名映射
|
|
77
|
+
_SCHEMA_FILES = {
|
|
78
|
+
"prd": "prd.schema.json",
|
|
79
|
+
"design-spec": "design-spec.schema.json",
|
|
80
|
+
"design": "design-spec.schema.json", # 别名
|
|
81
|
+
"test-cases": "test-cases.schema.json",
|
|
82
|
+
"test": "test-cases.schema.json", # 别名
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ============================================================
|
|
87
|
+
# 1. 契约头解析
|
|
88
|
+
# ============================================================
|
|
89
|
+
|
|
90
|
+
# MD 契约头: <!-- @contract prd v1\n platform: web\n ... -->
|
|
91
|
+
_MD_CONTRACT_RE = re.compile(
|
|
92
|
+
r"<!--\s*@contract\s+(\S+)(?:\s+v?(\d+))?\s*\n(.*?)-->",
|
|
93
|
+
re.DOTALL,
|
|
94
|
+
)
|
|
95
|
+
# 契约头内的 key: value 行
|
|
96
|
+
_KV_RE = re.compile(r"^\s*([\w\-]+)\s*:\s*(.+?)\s*$")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_contract_header(md_text: str) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""从 Markdown 正文提取 @contract 块。
|
|
101
|
+
|
|
102
|
+
格式 (见 contract-header.md):
|
|
103
|
+
<!-- @contract prd v1
|
|
104
|
+
platform: web
|
|
105
|
+
req-id: REQ-2025-0042
|
|
106
|
+
title: 保单批改功能
|
|
107
|
+
acceptance: [批改后实时生效, 批改需审批]
|
|
108
|
+
-->
|
|
109
|
+
"""
|
|
110
|
+
m = _MD_CONTRACT_RE.search(md_text)
|
|
111
|
+
if not m:
|
|
112
|
+
return None
|
|
113
|
+
ctype = m.group(1).strip()
|
|
114
|
+
version = int(m.group(2)) if m.group(2) else 1
|
|
115
|
+
body = m.group(3)
|
|
116
|
+
fields: Dict[str, Any] = {"type": ctype, "version": version}
|
|
117
|
+
for line in body.splitlines():
|
|
118
|
+
line = line.strip()
|
|
119
|
+
if not line:
|
|
120
|
+
continue
|
|
121
|
+
kv = _KV_RE.match(line)
|
|
122
|
+
if kv:
|
|
123
|
+
key = kv.group(1).strip()
|
|
124
|
+
val = kv.group(2).strip()
|
|
125
|
+
# acceptance: [a, b] → 列表
|
|
126
|
+
if val.startswith("[") and val.endswith("]"):
|
|
127
|
+
inner = val[1:-1]
|
|
128
|
+
fields[key] = [x.strip() for x in inner.split(",") if x.strip()]
|
|
129
|
+
else:
|
|
130
|
+
fields[key] = val
|
|
131
|
+
return fields
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def extract_json_contract(data: dict) -> Optional[Dict[str, Any]]:
|
|
135
|
+
"""从 JSON dict 提取 _contract 字段 (design-spec / test-cases 用)。"""
|
|
136
|
+
if isinstance(data, dict) and isinstance(data.get("_contract"), dict):
|
|
137
|
+
c = dict(data["_contract"])
|
|
138
|
+
# 补默认 type/version (schema 里 required, 但容错)
|
|
139
|
+
c.setdefault("type", c.get("type", "unknown"))
|
|
140
|
+
c.setdefault("version", 1)
|
|
141
|
+
return c
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def parse_contract(path) -> Tuple[Optional[Dict[str, Any]], str]:
|
|
146
|
+
"""解析任意文件的契约头。
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
(contract_dict, source_text)
|
|
150
|
+
- contract_dict: 解析出的契约字段, 无则 None
|
|
151
|
+
- source_text: 'md' / 'json' / 'unknown'
|
|
152
|
+
"""
|
|
153
|
+
path = Path(path)
|
|
154
|
+
if not path.is_file():
|
|
155
|
+
return None, "unknown"
|
|
156
|
+
name = path.name.lower()
|
|
157
|
+
try:
|
|
158
|
+
if name.endswith(".json"):
|
|
159
|
+
data = safe_read_json(path, default=None)
|
|
160
|
+
if data is None:
|
|
161
|
+
return None, "json"
|
|
162
|
+
return extract_json_contract(data), "json"
|
|
163
|
+
# 当作文本 (md 等)
|
|
164
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
165
|
+
hdr = extract_contract_header(text)
|
|
166
|
+
return hdr, "md"
|
|
167
|
+
except Exception:
|
|
168
|
+
return None, "unknown"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ============================================================
|
|
172
|
+
# 2. 轻量 schema 校验 (不引 jsonschema, stdlib 实现)
|
|
173
|
+
# ============================================================
|
|
174
|
+
|
|
175
|
+
def _load_schema(contract_type: str) -> Optional[dict]:
|
|
176
|
+
"""按契约类型加载对应 schema.json。"""
|
|
177
|
+
fname = _SCHEMA_FILES.get(contract_type)
|
|
178
|
+
if not fname:
|
|
179
|
+
return None
|
|
180
|
+
sp = SCHEMAS_DIR / fname
|
|
181
|
+
if not sp.is_file():
|
|
182
|
+
return None
|
|
183
|
+
try:
|
|
184
|
+
return json.loads(sp.read_text(encoding="utf-8"))
|
|
185
|
+
except Exception:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _check_field(field: str, value: Any, spec: dict, errors: list, warnings: list):
|
|
190
|
+
"""校验单个字段 (类型/枚举/正则/最小长度)。"""
|
|
191
|
+
if spec.get("const") is not None and value != spec["const"]:
|
|
192
|
+
errors.append("字段 {} 必须为常量 {!r}, 实际 {!r}".format(field, spec["const"], value))
|
|
193
|
+
return
|
|
194
|
+
t = spec.get("type")
|
|
195
|
+
if t == "string" and not isinstance(value, str):
|
|
196
|
+
errors.append("字段 {} 应为 string".format(field))
|
|
197
|
+
return
|
|
198
|
+
if t == "integer" and not isinstance(value, int):
|
|
199
|
+
errors.append("字段 {} 应为 integer".format(field))
|
|
200
|
+
return
|
|
201
|
+
if t == "array" and not isinstance(value, list):
|
|
202
|
+
errors.append("字段 {} 应为 array".format(field))
|
|
203
|
+
return
|
|
204
|
+
if t == "object" and not isinstance(value, dict):
|
|
205
|
+
errors.append("字段 {} 应为 object".format(field))
|
|
206
|
+
return
|
|
207
|
+
if "enum" in spec and value not in spec["enum"]:
|
|
208
|
+
errors.append("字段 {} 值 {!r} 不在允许范围 {}".format(field, value, spec["enum"]))
|
|
209
|
+
return
|
|
210
|
+
if "pattern" in spec and isinstance(value, str):
|
|
211
|
+
pat = spec["pattern"]
|
|
212
|
+
if not re.match(pat, value):
|
|
213
|
+
errors.append("字段 {} 值 {!r} 不匹配模式 {}".format(field, value, pat))
|
|
214
|
+
return
|
|
215
|
+
if "minLength" in spec and isinstance(value, str) and len(value) < spec["minLength"]:
|
|
216
|
+
warnings.append("字段 {} 长度 {} 小于最小 {} ".format(field, len(value), spec["minLength"]))
|
|
217
|
+
if "minItems" in spec and isinstance(value, list) and len(value) < spec["minItems"]:
|
|
218
|
+
warnings.append("字段 {} 项数 {} 小于最小 {} ".format(field, len(value), spec["minItems"]))
|
|
219
|
+
if "minimum" in spec and isinstance(value, (int, float)) and value < spec["minimum"]:
|
|
220
|
+
errors.append("字段 {} 值 {} 小于最小 {}".format(field, value, spec["minimum"]))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _validate_against_schema(data: dict, schema: dict) -> Tuple[list, list]:
|
|
224
|
+
"""按 schema 校验 data。返回 (errors, warnings)。"""
|
|
225
|
+
errors: list = []
|
|
226
|
+
warnings: list = []
|
|
227
|
+
required = schema.get("required", [])
|
|
228
|
+
props = schema.get("properties", {})
|
|
229
|
+
# required 检查
|
|
230
|
+
for f in required:
|
|
231
|
+
if f not in data:
|
|
232
|
+
errors.append("缺少必填字段: {}".format(f))
|
|
233
|
+
# 逐字段校验
|
|
234
|
+
for f, val in data.items():
|
|
235
|
+
if f in props:
|
|
236
|
+
_check_field(f, val, props[f], errors, warnings)
|
|
237
|
+
return errors, warnings
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ============================================================
|
|
241
|
+
# 3. 对外主接口
|
|
242
|
+
# ============================================================
|
|
243
|
+
|
|
244
|
+
def validate_contract(path) -> Result:
|
|
245
|
+
"""校验一个产出文件的契约。
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Result:
|
|
249
|
+
- ok + CODE.OK: 合法契约, 校验通过
|
|
250
|
+
- ok + WARN: 有警告 (如字段长度不足), 但可继续 (软门禁)
|
|
251
|
+
- fail: 有错误 (缺必填/类型错/枚举越界)
|
|
252
|
+
- ok + code='no_contract': 无契约头 (降级, 不当错误, 返回 None 提示)
|
|
253
|
+
|
|
254
|
+
降级原则: 无契约头或 schema 缺失 → 不当错误 (符合 contract-header.md)
|
|
255
|
+
"""
|
|
256
|
+
path = Path(path)
|
|
257
|
+
if not path.is_file():
|
|
258
|
+
return Result.failure(
|
|
259
|
+
"文件不存在: {}".format(path),
|
|
260
|
+
code=CODE.NOT_FOUND,
|
|
261
|
+
hint="检查路径",
|
|
262
|
+
)
|
|
263
|
+
contract, src = parse_contract(path)
|
|
264
|
+
if not contract:
|
|
265
|
+
# 无契约头: 降级, 不当错误
|
|
266
|
+
return Result.success(
|
|
267
|
+
msg="⚠️ {} 无 @contract 头, 已降级 (从头文件名/正文推断)".format(path.name),
|
|
268
|
+
code="no_contract",
|
|
269
|
+
)
|
|
270
|
+
ctype = contract.get("type", "")
|
|
271
|
+
schema = _load_schema(ctype)
|
|
272
|
+
if not schema:
|
|
273
|
+
return Result.warning(
|
|
274
|
+
"契约类型 {!r} 无对应 schema, 跳过校验".format(ctype),
|
|
275
|
+
code="no_schema",
|
|
276
|
+
)
|
|
277
|
+
errors, warnings = _validate_against_schema(contract, schema)
|
|
278
|
+
if errors:
|
|
279
|
+
return Result.failure(
|
|
280
|
+
"契约校验失败 ({}): {}".format(path.name, "; ".join(errors[:3])),
|
|
281
|
+
code=CODE.GENERIC,
|
|
282
|
+
hint="按 contracts/schemas/{} 修正".format(_SCHEMA_FILES.get(ctype, ctype + ".schema.json")),
|
|
283
|
+
)
|
|
284
|
+
if warnings:
|
|
285
|
+
return Result.warning(
|
|
286
|
+
"契约有警告 ({}): {}".format(path.name, "; ".join(warnings[:3])),
|
|
287
|
+
code=CODE.BELOW_THRESH,
|
|
288
|
+
)
|
|
289
|
+
return Result.success(
|
|
290
|
+
data={"type": ctype, "req-id": contract.get("req-id"), "platform": contract.get("platform")},
|
|
291
|
+
msg="✓ 契约校验通过 ({} v{})".format(ctype, contract.get("version", 1)),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ============================================================
|
|
296
|
+
# CLI
|
|
297
|
+
# ============================================================
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
if len(sys.argv) < 2:
|
|
301
|
+
print("用法: python contract.py <产出文件>")
|
|
302
|
+
print(" 校验 PRD/原型/spec/design-spec/test-cases 的 @contract 头")
|
|
303
|
+
sys.exit(1)
|
|
304
|
+
# 简单 CLI: 复用 graceful_error 风格
|
|
305
|
+
target = sys.argv[1]
|
|
306
|
+
r = validate_contract(target)
|
|
307
|
+
if r.is_warn:
|
|
308
|
+
print("[WARN] {}".format(r.msg))
|
|
309
|
+
if r.hint:
|
|
310
|
+
print(" -> {}".format(r.hint))
|
|
311
|
+
sys.exit(0)
|
|
312
|
+
if not r.ok:
|
|
313
|
+
print("[FAIL] {}".format(r.msg))
|
|
314
|
+
if r.hint:
|
|
315
|
+
print(" -> {}".format(r.hint))
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
print(r.msg)
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
# 新增埋点: 在这里加一行 → learn.py 计数 + learn_aggregate.py SQL 自动生效
|
|
19
19
|
EVENT_TYPES = {
|
|
20
20
|
# PRD 链路
|
|
21
|
-
'prd_accepted': {'stat': 'prds_generated', 'skill': '
|
|
22
|
-
'prd_rejected': {'stat': 'rejections', 'skill': '
|
|
21
|
+
'prd_accepted': {'stat': 'prds_generated', 'skill': 'prd-generator', 'label': 'PRD 发布'},
|
|
22
|
+
'prd_rejected': {'stat': 'rejections', 'skill': 'prd-generator', 'label': 'PRD 打回'},
|
|
23
23
|
'review_done': {'stat': 'reviews_done', 'skill': 'prd-review', 'label': 'PRD 评审'},
|
|
24
24
|
# 设计链路
|
|
25
25
|
'design_import': {'stat': 'designs_imported','skill': 'design-import', 'label': '设计录入'},
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
result.py - 统一结果类型 + 优雅错误处理
|
|
4
|
+
|
|
5
|
+
治"容错率低"的底层基建。现状各脚本错误风格极不统一:
|
|
6
|
+
- 裸 except Exception: pass 吞异常 (build_style_index / eval_prd / team_sync)
|
|
7
|
+
- exit code 含义不一致 (eval_prd 1=不达标或文件不存在; team_sync 0/1/2/3 四档)
|
|
8
|
+
- 调用方只能 parse 中文文案, 无法程序化决策
|
|
9
|
+
|
|
10
|
+
本模块引入:
|
|
11
|
+
1. Result 数据类: ok / code / msg / data, 语义化 code
|
|
12
|
+
2. graceful_error 装饰器: 把 Result/异常翻译成"人类/AI 可读文本 + 一致 exit code"
|
|
13
|
+
3. 统一 exit code 表 (替代散落各处的魔法数字)
|
|
14
|
+
|
|
15
|
+
设计原则 (与方案一致):
|
|
16
|
+
- 失败不崩, 输出可读诊断 + 建议下一步
|
|
17
|
+
- 调用方能据 code 程序化决策, 不靠 parse 中文
|
|
18
|
+
- eval_prd 门禁从"硬阻塞"改"软门禁" (WARN 带警告发布)
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from foundation.data.result import Result, OK, fail, warn, graceful_error
|
|
22
|
+
|
|
23
|
+
@graceful_error
|
|
24
|
+
def main():
|
|
25
|
+
r = do_something()
|
|
26
|
+
if not r.ok:
|
|
27
|
+
return r # 装饰器翻译成可读输出 + exit code
|
|
28
|
+
return Result.ok(data=r.data)
|
|
29
|
+
|
|
30
|
+
Exit code 约定 (全脚本统一, 替代散落魔法数字):
|
|
31
|
+
0 OK 成功
|
|
32
|
+
1 GENERIC 通用失败 (未分类)
|
|
33
|
+
2 BUSY 资源占用 (如 kg_db 被占)
|
|
34
|
+
3 CONFLICT 冲突需人工 (如 team_sync SYNC_CONFLICT)
|
|
35
|
+
4 AUTHZ 身份/权限拒绝 (与 identity.EXIT_AUTHZ 对齐)
|
|
36
|
+
5 NOT_FOUND 文件/资源不存在
|
|
37
|
+
6 INDEX_MISSING 索引缺失 (需先 init/doctor)
|
|
38
|
+
7 BELOW_THRESH 门禁未达标 (eval_prd, 软门禁可带警告继续)
|
|
39
|
+
8 NETWORK 网络/外部服务不可达
|
|
40
|
+
9 DEPS_MISSING 依赖未装 (pip 包缺失)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import functools
|
|
44
|
+
import sys
|
|
45
|
+
import traceback
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from typing import Any, Optional
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Result",
|
|
51
|
+
"OK",
|
|
52
|
+
"ResultError",
|
|
53
|
+
"ok",
|
|
54
|
+
"fail",
|
|
55
|
+
"warn",
|
|
56
|
+
"graceful_error",
|
|
57
|
+
"EXIT",
|
|
58
|
+
"code_to_exit",
|
|
59
|
+
"CODE",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ============================================================
|
|
64
|
+
# 语义化 code 常量 (调用方据此程序化决策)
|
|
65
|
+
# ============================================================
|
|
66
|
+
|
|
67
|
+
class CODE:
|
|
68
|
+
"""结果 code 常量。小写字符串, 语义化, 跨脚本一致。"""
|
|
69
|
+
OK = "ok"
|
|
70
|
+
GENERIC = "generic" # 通用未分类
|
|
71
|
+
BUSY = "busy" # 资源占用 (kg_db 锁)
|
|
72
|
+
CONFLICT = "conflict" # 冲突需人工
|
|
73
|
+
AUTHZ = "authz" # 身份/权限
|
|
74
|
+
NOT_FOUND = "not_found" # 文件/资源不存在
|
|
75
|
+
INDEX_MISSING = "index_missing" # 索引缺失
|
|
76
|
+
BELOW_THRESH = "below_thresh" # 门禁未达标
|
|
77
|
+
NETWORK = "network" # 网络不可达
|
|
78
|
+
DEPS_MISSING = "deps_missing" # 依赖未装
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================
|
|
82
|
+
# code → exit code 映射 (一处定义, 全脚本统一)
|
|
83
|
+
# ============================================================
|
|
84
|
+
|
|
85
|
+
EXIT = {
|
|
86
|
+
CODE.OK: 0,
|
|
87
|
+
CODE.GENERIC: 1,
|
|
88
|
+
CODE.BUSY: 2,
|
|
89
|
+
CODE.CONFLICT: 3,
|
|
90
|
+
CODE.AUTHZ: 4,
|
|
91
|
+
CODE.NOT_FOUND: 5,
|
|
92
|
+
CODE.INDEX_MISSING: 6,
|
|
93
|
+
CODE.BELOW_THRESH: 7,
|
|
94
|
+
CODE.NETWORK: 8,
|
|
95
|
+
CODE.DEPS_MISSING: 9,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def code_to_exit(code: str) -> int:
|
|
100
|
+
"""code → exit code, 未知 code 回退 1。"""
|
|
101
|
+
return EXIT.get(code, 1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================
|
|
105
|
+
# Result 数据类
|
|
106
|
+
# ============================================================
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class Result:
|
|
110
|
+
"""统一结果。
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
ok: 是否成功
|
|
114
|
+
code: 语义化 code (见 CODE 常量), 调用方据此决策
|
|
115
|
+
msg: 人类可读消息
|
|
116
|
+
data: 成功时携带的数据 (任意)
|
|
117
|
+
hint: 失败时的"下一步建议" (可选, 输出给用户/AI)
|
|
118
|
+
"""
|
|
119
|
+
ok: bool
|
|
120
|
+
code: str = CODE.GENERIC
|
|
121
|
+
msg: str = ""
|
|
122
|
+
data: Any = None
|
|
123
|
+
hint: Optional[str] = None
|
|
124
|
+
|
|
125
|
+
# ---- 工厂方法 ----
|
|
126
|
+
@classmethod
|
|
127
|
+
def success(cls, data: Any = None, msg: str = "", code: str = CODE.OK) -> "Result":
|
|
128
|
+
return cls(ok=True, code=code, msg=msg, data=data)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def failure(cls, msg: str, code: str = CODE.GENERIC,
|
|
132
|
+
hint: Optional[str] = None, data: Any = None) -> "Result":
|
|
133
|
+
return cls(ok=False, code=code, msg=msg, hint=hint, data=data)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def warning(cls, msg: str, code: str = CODE.BELOW_THRESH,
|
|
137
|
+
hint: Optional[str] = None, data: Any = None) -> "Result":
|
|
138
|
+
"""WARN: 软门禁/带警告继续。ok=True (可继续), 但带警告。"""
|
|
139
|
+
return cls(ok=True, code=code, msg=msg, hint=hint, data=data)
|
|
140
|
+
|
|
141
|
+
# ---- 便捷判断 ----
|
|
142
|
+
@property
|
|
143
|
+
def is_warn(self) -> bool:
|
|
144
|
+
"""是否为"成功但有警告"(如门禁 WARN 档)。"""
|
|
145
|
+
return self.ok and self.code != CODE.OK
|
|
146
|
+
|
|
147
|
+
def __bool__(self) -> bool:
|
|
148
|
+
return self.ok
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# 模块级便捷别名 (常见用法)
|
|
152
|
+
OK = Result.success()
|
|
153
|
+
ResultError = type("ResultError", (Exception,), {}) # 预留: 未来 raise Result(r)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ok(data: Any = None, msg: str = "") -> Result:
|
|
157
|
+
return Result.success(data=data, msg=msg)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def fail(msg: str, code: str = CODE.GENERIC, hint: Optional[str] = None) -> Result:
|
|
161
|
+
return Result.failure(msg=msg, code=code, hint=hint)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def warn(msg: str, code: str = CODE.BELOW_THRESH, hint: Optional[str] = None) -> Result:
|
|
165
|
+
return Result.warning(msg=msg, code=code, hint=hint)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ============================================================
|
|
169
|
+
# graceful_error 装饰器
|
|
170
|
+
# ============================================================
|
|
171
|
+
|
|
172
|
+
def _emit(r: Result, *, exit_on_fail: bool = True) -> int:
|
|
173
|
+
"""把 Result 翻译成可读输出 + 返回 exit code。"""
|
|
174
|
+
if r.ok and not r.is_warn:
|
|
175
|
+
if r.msg:
|
|
176
|
+
print(r.msg)
|
|
177
|
+
return 0
|
|
178
|
+
if r.is_warn:
|
|
179
|
+
# WARN: 打印警告但不当失败
|
|
180
|
+
print("[WARN] {}".format(r.msg))
|
|
181
|
+
if r.hint:
|
|
182
|
+
print(" -> {}".format(r.hint))
|
|
183
|
+
return code_to_exit(r.code) if False else 0 # WARN 视为成功 (exit 0)
|
|
184
|
+
# FAIL
|
|
185
|
+
tag = r.code.upper() if r.code != CODE.GENERIC else "ERROR"
|
|
186
|
+
print("[{}] {}".format(tag, r.msg))
|
|
187
|
+
if r.hint:
|
|
188
|
+
print(" -> {}".format(r.hint))
|
|
189
|
+
if exit_on_fail:
|
|
190
|
+
sys.exit(code_to_exit(r.code))
|
|
191
|
+
return code_to_exit(r.code)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def graceful_error(func):
|
|
195
|
+
"""装饰 main(): 统一把返回值 (Result/None/int) 翻译成可读输出 + exit code。
|
|
196
|
+
|
|
197
|
+
- 返回 Result: 按 ok/code 翻译
|
|
198
|
+
- 返回 int: 直接当 exit code
|
|
199
|
+
- 返回 None: 视为成功 (exit 0)
|
|
200
|
+
- 抛异常: 捕获, 输出诊断 + traceback (失败不崩 → 但 main 层崩溃要可见)
|
|
201
|
+
"""
|
|
202
|
+
@functools.wraps(func)
|
|
203
|
+
def wrapper(*args, **kwargs):
|
|
204
|
+
try:
|
|
205
|
+
ret = func(*args, **kwargs)
|
|
206
|
+
except SystemExit:
|
|
207
|
+
raise
|
|
208
|
+
except KeyboardInterrupt:
|
|
209
|
+
print("\n[中断] 用户取消")
|
|
210
|
+
sys.exit(130)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# 未捕获异常: 输出诊断 + 简短 traceback, 不静默吞
|
|
213
|
+
print("[ERROR] 未预期异常: {}".format(e))
|
|
214
|
+
print(" -> 这是 bug, 请反馈。traceback:")
|
|
215
|
+
traceback.print_exc()
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
if isinstance(ret, Result):
|
|
218
|
+
return _emit(ret)
|
|
219
|
+
if isinstance(ret, int):
|
|
220
|
+
sys.exit(ret)
|
|
221
|
+
# None / 其他: 成功
|
|
222
|
+
return 0
|
|
223
|
+
return wrapper
|