@hupan56/wlkj 2.7.12 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -0,0 +1,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 common.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 .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)
@@ -147,9 +147,10 @@ def init_developer(name: str, role: str | None = None,
147
147
  return False
148
148
 
149
149
  # 2. 创建个人工作空间 workspace/members/<name>/
150
+ # v3.0 M3: 个人目录 = member.json + drafts + outputs + journal + .private
150
151
  personal = repo_root / "workspace" / "members" / name
151
152
  try:
152
- for sub in ("journal", "drafts", "inbox"):
153
+ for sub in ("journal", "drafts", "outputs", ".private", ".private/secrets"):
153
154
  (personal / sub).mkdir(parents=True, exist_ok=True)
154
155
  except (OSError, IOError) as e:
155
156
  print(f"Error: Failed to create workspace directory: {e}", file=sys.stderr)
@@ -57,18 +57,19 @@ THEME_COLORS = {
57
57
 
58
58
 
59
59
  def _load_config():
60
- """加载飞书配置 (config.yaml + 环境变量)。"""
61
- # 读 config.yaml 的 feishu 段
60
+ """加载飞书配置 (config.yaml + 环境变量)。
61
+
62
+ v3.0: 优先用 Config 单例 (带缓存), 失败回退文本解析。
63
+ """
62
64
  config = {}
63
65
  try:
64
- import yaml
65
- from .paths import get_repo_root
66
- cfg_path = os.path.join(str(get_repo_root()), '.qoder', 'config.yaml')
67
- with open(cfg_path, encoding='utf-8') as f:
68
- full = yaml.safe_load(f) or {}
69
- config = full.get('feishu') or {}
66
+ # 优先: Config 单例 (mtime 缓存, 避免每次重读)
67
+ from .config import Config, SECTIONS
68
+ config = Config.load().section(SECTIONS.FEISHU, {})
70
69
  except Exception:
71
- # PyYAML 不可用, 用简单解析
70
+ pass
71
+ if not config:
72
+ # 回退: 简单文本解析 (PyYAML 不可用时)
72
73
  try:
73
74
  from .paths import get_repo_root
74
75
  cfg_path = os.path.join(str(get_repo_root()), '.qoder', 'config.yaml')
@@ -0,0 +1,159 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ guard.py - 角色能力边界守卫 (解耦的执行点)
4
+
5
+ 把角色从"标签"变成"边界"。各角色在能力边界内自治, 互不越界 = 解耦。
6
+
7
+ 现状: role.py 只查询角色名, config.yaml 的 permissions 声明了但从不强制。
8
+ 本次: guard.py 在"产出动作前"校验角色能否产出该契约类型, 越权则降级 (不报错)。
9
+
10
+ 设计原则 (与方案一致):
11
+ - 降级而非阻塞: 越权不报错/不崩溃, 而是降级 (转给有权限的人 / 标记待确认)
12
+ 这与契约层的"降级不停摆"哲学一致
13
+ - 软边界: 默认不强制 (ensure=False), 调用方显式 ensure=True 才硬拦
14
+ - 可观测: 每次越权都留痕 (返回 Result + hint), 让 AI/用户知道发生了什么
15
+
16
+ 核心 API:
17
+ check_produce(contract_type, role=None, ensure=False) -> Result
18
+ 检查当前角色能否产出某契约
19
+ ensure=False (默认): 越权返回 WARN (降级提示, 不阻塞)
20
+ ensure=True: 越权返回 FAIL (硬拦, 给破坏性操作用)
21
+ current_role() -> str | None
22
+ 当前角色名 (identity.get_role 的便捷封装)
23
+
24
+ Usage:
25
+ from common.guard import check_produce
26
+ r = check_produce('unit-test')
27
+ if not r.ok:
28
+ print(r.hint) # "你的角色 pm 不能产出 unit-test, 需 dev/test 角色"
29
+ return # 降级: 不执行产出
30
+ # ... 继续产出 unit-test
31
+ """
32
+
33
+ import os
34
+ import sys
35
+
36
+ try:
37
+ from .result import Result, CODE
38
+ from .identity import get_role, can_produce, can_consume, get_role_config
39
+ except ImportError:
40
+ _COMMON_DIR = os.path.dirname(os.path.abspath(__file__))
41
+ if _COMMON_DIR not in sys.path:
42
+ sys.path.insert(0, _COMMON_DIR)
43
+ from result import Result, CODE
44
+ from identity import get_role, can_produce, can_consume, get_role_config
45
+
46
+ __all__ = [
47
+ "check_produce",
48
+ "check_consume",
49
+ "current_role",
50
+ "suggest_role_for",
51
+ ]
52
+
53
+ # 角色中文名映射 (给提示文案用)
54
+ _ROLE_CN = {
55
+ "pm": "产品经理",
56
+ "design": "设计师",
57
+ "dev": "开发",
58
+ "test": "测试",
59
+ "admin": "管理员",
60
+ }
61
+
62
+
63
+ def current_role() -> str:
64
+ """当前角色名。未初始化/未注册 → 回退 'pm' (软引导, 与 role.py 一致)。"""
65
+ return get_role() or "pm"
66
+
67
+
68
+ def _role_cn(role: str) -> str:
69
+ return _ROLE_CN.get(role, role)
70
+
71
+
72
+ def suggest_role_for(contract_type: str) -> list:
73
+ """建议"哪些角色能产出该契约", 给越权提示用。"""
74
+ # 反查 config.yaml 哪些角色的 can_produce 含此类型或 '*'
75
+ try:
76
+ from .identity import _load_roles_config
77
+ except ImportError:
78
+ from identity import _load_roles_config
79
+ roles_cfg = _load_roles_config()
80
+ out = []
81
+ for r, cfg in roles_cfg.items():
82
+ caps = list(cfg.get("can_produce", []) or [])
83
+ if "*" in caps or contract_type in caps:
84
+ out.append(r)
85
+ return out
86
+
87
+
88
+ def check_produce(
89
+ contract_type: str,
90
+ role: str = None,
91
+ ensure: bool = False,
92
+ ) -> Result:
93
+ """检查角色能否产出某契约类型。
94
+
95
+ Args:
96
+ contract_type: 契约类型 (prd/prototype/spec/design-spec/code/unit-test/test-report)
97
+ role: 角色名, 不传则用当前角色
98
+ ensure: False=越权返回WARN(降级); True=越权返回FAIL(硬拦)
99
+
100
+ Returns:
101
+ Result.ok + CODE.OK: 允许产出
102
+ Result (WARN): 越权但降级 (ensure=False), 带 hint 提示该找谁
103
+ Result (FAIL): 越权且硬拦 (ensure=True)
104
+ """
105
+ role = role or current_role()
106
+ if can_produce(role, contract_type):
107
+ return Result.success(msg="✓ {} 可产出 {}".format(_role_cn(role), contract_type))
108
+ # 越权
109
+ suggest = suggest_role_for(contract_type)
110
+ suggest_cn = "、".join(_role_cn(r) for r in suggest) if suggest else "对应角色"
111
+ hint = "你的角色({})不能产出 {}, 需 {} 角色。用 /wl-init 切换或转交。".format(
112
+ _role_cn(role), contract_type, suggest_cn
113
+ )
114
+ if ensure:
115
+ return Result.failure(
116
+ "[越权] {} 不能产出 {}".format(_role_cn(role), contract_type),
117
+ code=CODE.AUTHZ,
118
+ hint=hint,
119
+ )
120
+ # 降级: 不阻塞, 但强烈提示
121
+ return Result.warning(
122
+ "⚠️ {} 通常不产出 {} (建议 {})".format(_role_cn(role), contract_type, suggest_cn),
123
+ code=CODE.AUTHZ,
124
+ hint=hint,
125
+ )
126
+
127
+
128
+ def check_consume(
129
+ capability: str,
130
+ role: str = None,
131
+ ) -> Result:
132
+ """检查角色能否读取/消费某能力。越权则只读降级 (永远不阻塞)。"""
133
+ role = role or current_role()
134
+ if can_consume(role, capability):
135
+ return Result.success(msg="✓ {} 可消费 {}".format(_role_cn(role), capability))
136
+ return Result.warning(
137
+ "⚠️ {} 读取 {} 超出常规职责 (只读降级)".format(_role_cn(role), capability),
138
+ code=CODE.AUTHZ,
139
+ hint="这是软提示, 仍可继续 (越权只读降级, 不阻塞)",
140
+ )
141
+
142
+
143
+ # ============================================================
144
+ # CLI (供 skill 脚本诊断用)
145
+ # ============================================================
146
+
147
+ if __name__ == "__main__":
148
+ import json
149
+ role = current_role()
150
+ print("当前角色: {} ({})".format(role, _role_cn(role)))
151
+ cfg = get_role_config(role)
152
+ print(" can_produce: {}".format(cfg.get("can_produce", [])))
153
+ print(" can_consume: {}".format(cfg.get("can_consume", [])))
154
+ if len(sys.argv) > 1:
155
+ ct = sys.argv[1]
156
+ r = check_produce(ct)
157
+ print("\n能否产出 {}: {}".format(ct, "能" if r.ok and not r.is_warn else "否"))
158
+ if r.hint:
159
+ print(" -> {}".format(r.hint))
@@ -65,9 +65,16 @@ __all__ = [
65
65
  "verify_signature",
66
66
  "require_member",
67
67
  "ensure_current_member",
68
+ # ★ 角色能力 (v3.0 合并自 role.py) ★
69
+ "get_role",
70
+ "get_role_config",
71
+ "get_capabilities",
72
+ "get_permissions",
73
+ "can_produce",
74
+ "can_consume",
68
75
  ]
69
76
 
70
- # 退出码 (全脚本统一): 4 = authz_denied
77
+ # 退出码 (与 common/result.py 的 EXIT[AUTHZ] 对齐): 4 = authz_denied
71
78
  EXIT_AUTHZ = 4
72
79
 
73
80
  # 名字合法字符: 字母/数字/中文/下划线/短横, 2-32 字符
@@ -96,7 +103,12 @@ def _member_json(name: str, repo_root: Optional[Union[str, Path]] = None) -> Pat
96
103
 
97
104
 
98
105
  def _key_file(name: str, repo_root: Optional[Union[str, Path]] = None) -> Path:
99
- return _member_dir(name, repo_root) / ".signing_key"
106
+ """签名密钥路径 (v3.0: 收进 .private/signing_key, 向后兼容老 .signing_key)。"""
107
+ try:
108
+ from .paths import get_private_file
109
+ return get_private_file("signing_key", name)
110
+ except Exception:
111
+ return _member_dir(name, repo_root) / ".signing_key"
100
112
 
101
113
 
102
114
  # ============================================================
@@ -288,3 +300,110 @@ def ensure_current_member(repo_root: Optional[Union[str, Path]] = None) -> Optio
288
300
  if name and verify_member(name, repo_root):
289
301
  return name
290
302
  return None
303
+
304
+
305
+ # ============================================================
306
+ # 角色能力 (v3.0 合并自 role.py —— 角色从"标签"升级为"能力边界")
307
+ #
308
+ # config.yaml 的 roles 段升级为能力声明:
309
+ # roles.<role>.can_produce: [prd, prototype, ...] 允许产出的契约类型
310
+ # roles.<role>.can_consume: ["*", prd, ...] 允许读取的能力
311
+ # guard.py 据此强制边界: 越权降级 (不报错), 各角色在边界内自治 = 解耦
312
+ #
313
+ # 向后兼容: get_role / get_permissions 保留 (老脚本可继续用)
314
+ # ============================================================
315
+
316
+ def _load_roles_config(repo_root: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
317
+ """读 config.yaml 的 roles 段。失败返回 {} (软降级, 不崩)。"""
318
+ if repo_root is None:
319
+ repo_root = get_repo_root()
320
+ cfg = Path(repo_root) / ".qoder" / "config.yaml"
321
+ if not cfg.is_file():
322
+ return {}
323
+ try:
324
+ import yaml
325
+ with open(cfg, encoding="utf-8") as f:
326
+ data = yaml.safe_load(f) or {}
327
+ return data.get("roles", {}) or {}
328
+ except Exception:
329
+ return {}
330
+
331
+
332
+ def get_role(name: Optional[str] = None, repo_root: Optional[Union[str, Path]] = None) -> Optional[str]:
333
+ """返回成员的角色名 (pm/design/dev/test/admin)。
334
+
335
+ 优先级:
336
+ 1. .developer 里记录的 role (init 时写)
337
+ 2. member.json 里的 role
338
+ 3. None (未注册)
339
+ 无角色默认回退 'pm' (软引导, 不报错) —— 与原 role.py 行为一致。
340
+ """
341
+ if not name:
342
+ # 从 .developer 读
343
+ try:
344
+ from paths import get_developer, get_developer_info
345
+ except ImportError:
346
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
347
+ from paths import get_developer, get_developer_info # type: ignore
348
+ info = get_developer_info(repo_root) if "get_developer_info" in dir() else None
349
+ if isinstance(info, dict) and info.get("role"):
350
+ return info["role"]
351
+ name = get_developer(repo_root)
352
+ if not name:
353
+ return None
354
+ m = get_member(name, repo_root)
355
+ return m.get("role") if m else None
356
+
357
+
358
+ def get_role_config(role: Optional[str], repo_root: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
359
+ """返回某角色的完整配置 (can_produce/can_consume/commands/permissions/name)。
360
+
361
+ 角色不存在或 config 缺失 → 返回 {} (软降级)。
362
+ """
363
+ if not role:
364
+ return {}
365
+ return _load_roles_config(repo_root).get(role, {}) or {}
366
+
367
+
368
+ def get_permissions(role: Optional[str], repo_root: Optional[Union[str, Path]] = None) -> list:
369
+ """遗留 API: 返回角色的 permissions 列表 (向后兼容)。空则 []。"""
370
+ return list(get_role_config(role, repo_root).get("permissions", []) or [])
371
+
372
+
373
+ def get_capabilities(
374
+ role: Optional[str], kind: str = "produce", repo_root: Optional[Union[str, Path]] = None
375
+ ) -> list:
376
+ """返回角色的能力列表。
377
+
378
+ Args:
379
+ role: 角色名
380
+ kind: 'produce' (能产出哪些契约) 或 'consume' (能读哪些)
381
+ Returns:
382
+ 能力列表, 含 '*' 表示无限制。未知角色返回 [] (从严, 无能力)。
383
+ """
384
+ key = "can_produce" if kind == "produce" else "can_consume"
385
+ caps = get_role_config(role, repo_root).get(key, [])
386
+ return list(caps) if caps else []
387
+
388
+
389
+ def _has(cap_list: list, contract_type: str) -> bool:
390
+ """判断 contract_type 是否在能力列表内 ('*' 通配)。"""
391
+ if not cap_list:
392
+ return False
393
+ if "*" in cap_list:
394
+ return True
395
+ return contract_type in cap_list
396
+
397
+
398
+ def can_produce(
399
+ role: Optional[str], contract_type: str, repo_root: Optional[Union[str, Path]] = None
400
+ ) -> bool:
401
+ """该角色能否产出指定契约类型? guard.py 的核心查询。"""
402
+ return _has(get_capabilities(role, "produce", repo_root), contract_type)
403
+
404
+
405
+ def can_consume(
406
+ role: Optional[str], capability: str, repo_root: Optional[Union[str, Path]] = None
407
+ ) -> bool:
408
+ """该角色能否读取/消费指定能力? 越权则只读降级。"""
409
+ return _has(get_capabilities(role, "consume", repo_root), capability)