guarantee-based-coding 0.2.0__py3-none-any.whl
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.
- gbc/__init__.py +21 -0
- gbc/app/__init__.py +14 -0
- gbc/app/assets.py +38 -0
- gbc/app/config/__init__.py +14 -0
- gbc/app/config/backups.py +28 -0
- gbc/app/config/base.py +29 -0
- gbc/app/config/executor.py +132 -0
- gbc/app/config/project.py +35 -0
- gbc/app/core/__init__.py +14 -0
- gbc/app/core/env.py +64 -0
- gbc/app/core/executor.py +116 -0
- gbc/app/core/guarantee.py +338 -0
- gbc/app/i18n/__init__.py +43 -0
- gbc/app/i18n/lang.py +88 -0
- gbc/app/i18n/translate.py +80 -0
- gbc/app/intent/__init__.py +20 -0
- gbc/app/intent/base.py +308 -0
- gbc/app/intent/cli.py +124 -0
- gbc/app/intent/editor.py +93 -0
- gbc/app/interface/__init__.py +14 -0
- gbc/app/interface/base.py +851 -0
- gbc/app/interface/cli.py +585 -0
- gbc/app/interface/mcp.py +616 -0
- gbc/app/models/__init__.py +14 -0
- gbc/app/models/errors.py +179 -0
- gbc/app/models/meta.py +92 -0
- gbc/app/models/verify.py +63 -0
- gbc/app/utils/__init__.py +14 -0
- gbc/app/utils/file_utils.py +24 -0
- gbc/app/utils/gbc_md.py +121 -0
- gbc/app/utils/json_model_operator.py +85 -0
- gbc/app/utils/safe_file_writer.py +158 -0
- gbc/assets/editor/index.html +299 -0
- gbc/assets/i18n/catalog/en.json +52 -0
- gbc/assets/i18n/catalog/zh.json +52 -0
- gbc/assets/i18n/texts/rules.en.md +30 -0
- gbc/assets/i18n/texts/rules.zh.md +24 -0
- gbc/assets/i18n/texts/setup.en.md +74 -0
- gbc/assets/i18n/texts/setup.zh.md +69 -0
- gbc/assets/skills/README.md +16 -0
- gbc/assets/skills/gbc-cli/SKILL.md +143 -0
- gbc/entry.py +126 -0
- guarantee_based_coding-0.2.0.dist-info/METADATA +108 -0
- guarantee_based_coding-0.2.0.dist-info/RECORD +48 -0
- guarantee_based_coding-0.2.0.dist-info/WHEEL +5 -0
- guarantee_based_coding-0.2.0.dist-info/entry_points.txt +2 -0
- guarantee_based_coding-0.2.0.dist-info/licenses/LICENSE +202 -0
- guarantee_based_coding-0.2.0.dist-info/top_level.txt +1 -0
gbc/app/interface/cli.py
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# Copyright 2026 Jesse-x86
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# app/interface/cli.py
|
|
16
|
+
"""GBC 命令行:human/agent 都能用的命令面,镜像 base 层能力(与 mcp 工具一一对应)。"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
from gbc.app.interface import base
|
|
27
|
+
from gbc.app.models.errors import (
|
|
28
|
+
GBCError,
|
|
29
|
+
IllegalOperationError,
|
|
30
|
+
ConfigError,
|
|
31
|
+
ExecutorError,
|
|
32
|
+
GuaranteeError,
|
|
33
|
+
GuaranteeTestFailedError,
|
|
34
|
+
GuaranteeHasDependentsError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# ======== App & Console ========
|
|
38
|
+
|
|
39
|
+
app = typer.Typer(help="cli.app.help")
|
|
40
|
+
guarantee_app = typer.Typer(help="cli.guarantee.help")
|
|
41
|
+
dep_app = typer.Typer(help="cli.dep.help")
|
|
42
|
+
verify_app = typer.Typer(help="cli.verify.help")
|
|
43
|
+
doctor_app = typer.Typer(help="cli.doctor.help")
|
|
44
|
+
executor_app = typer.Typer(help="cli.executor.help")
|
|
45
|
+
|
|
46
|
+
app.add_typer(guarantee_app, name="guarantee")
|
|
47
|
+
app.add_typer(dep_app, name="dep")
|
|
48
|
+
app.add_typer(verify_app, name="verify")
|
|
49
|
+
app.add_typer(doctor_app, name="doctor")
|
|
50
|
+
app.add_typer(executor_app, name="executor")
|
|
51
|
+
|
|
52
|
+
console = Console()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ======== 全局语言入口 ========
|
|
56
|
+
|
|
57
|
+
@app.callback()
|
|
58
|
+
def _main(
|
|
59
|
+
lang: Optional[str] = typer.Option(None, "--lang", help="cli.option.lang.help"),
|
|
60
|
+
):
|
|
61
|
+
"""在任何子命令执行前固定语言,使报错与提示都本地化。"""
|
|
62
|
+
from gbc.app.i18n import set_lang, resolve_lang
|
|
63
|
+
set_lang(resolve_lang(lang))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ======== i18n 帮助文本延迟翻译 ========
|
|
67
|
+
|
|
68
|
+
import sys as _sys
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _lang_from_argv():
|
|
72
|
+
"""从 sys.argv 抓 --lang 值;没有或值非法则返回 None。
|
|
73
|
+
|
|
74
|
+
处理 --help 是 eager option 的场景:Click 在 --help 触发时可能尚未解析 --lang
|
|
75
|
+
(eager pass 优先),此时 ctx.params 里没有 lang。只好退而求其次扫 argv。
|
|
76
|
+
|
|
77
|
+
支持两种语法:`--lang zh`(分两 token)与 `--lang=zh`(合在一 token)。
|
|
78
|
+
"""
|
|
79
|
+
for i, arg in enumerate(_sys.argv):
|
|
80
|
+
if arg == "--lang" and i + 1 < len(_sys.argv):
|
|
81
|
+
val = _sys.argv[i + 1]
|
|
82
|
+
if val and not val.startswith("-"):
|
|
83
|
+
return val
|
|
84
|
+
return None
|
|
85
|
+
if arg.startswith("--lang="):
|
|
86
|
+
val = arg.split("=", 1)[1]
|
|
87
|
+
if val and not val.startswith("-"):
|
|
88
|
+
return val
|
|
89
|
+
return None
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ======== Help i18n: save / translate / render / restore ========
|
|
94
|
+
|
|
95
|
+
def _snapshot_tree_helps(node):
|
|
96
|
+
"""深度保存 Click 节点树所有 .help 属性,返回可还原的快照 dict。
|
|
97
|
+
|
|
98
|
+
覆盖:(a) 节点自身的 .help (b) 所有 param(Argument/Option) 的 .help
|
|
99
|
+
(c) 所有子命令(递归)。
|
|
100
|
+
"""
|
|
101
|
+
snap: dict = {"_help": node.help if hasattr(node, "help") else None}
|
|
102
|
+
if hasattr(node, "params"):
|
|
103
|
+
snap["_params"] = [p.help for p in node.params]
|
|
104
|
+
else:
|
|
105
|
+
snap["_params"] = []
|
|
106
|
+
if hasattr(node, "commands"):
|
|
107
|
+
snap["_subs"] = {name: _snapshot_tree_helps(cmd) for name, cmd in node.commands.items()}
|
|
108
|
+
else:
|
|
109
|
+
snap["_subs"] = {}
|
|
110
|
+
return snap
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _restore_tree_helps(node, snap):
|
|
114
|
+
"""从快照恢复 Click 节点树的全部 .help 属性(_snapshot_tree_helps 的逆操作)。"""
|
|
115
|
+
if snap is None:
|
|
116
|
+
return
|
|
117
|
+
if hasattr(node, "help") and snap.get("_help") is not None:
|
|
118
|
+
node.help = snap["_help"]
|
|
119
|
+
if hasattr(node, "params"):
|
|
120
|
+
saved_params = snap.get("_params", [])
|
|
121
|
+
for i, p in enumerate(node.params):
|
|
122
|
+
if i < len(saved_params):
|
|
123
|
+
p.help = saved_params[i]
|
|
124
|
+
if hasattr(node, "commands"):
|
|
125
|
+
saved_subs = snap.get("_subs", {})
|
|
126
|
+
for name, cmd in node.commands.items():
|
|
127
|
+
if name in saved_subs:
|
|
128
|
+
_restore_tree_helps(cmd, saved_subs[name])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _translate_tree_helps(node):
|
|
132
|
+
"""深度遍历 Click 节点树,把 i18n key 翻译成当前语言串(原地改写 .help)。
|
|
133
|
+
|
|
134
|
+
覆盖 Group/Command 的 .help 与 Parameter(Argument/Option) 的 .help。
|
|
135
|
+
非 key(如硬编码中文)由 t() 原样返回,不会被误翻。
|
|
136
|
+
"""
|
|
137
|
+
from gbc.app.i18n import t
|
|
138
|
+
|
|
139
|
+
if hasattr(node, "help") and node.help is not None and isinstance(node.help, str):
|
|
140
|
+
translated = t(node.help)
|
|
141
|
+
if translated != node.help:
|
|
142
|
+
node.help = translated
|
|
143
|
+
|
|
144
|
+
if hasattr(node, "params"):
|
|
145
|
+
for param in node.params:
|
|
146
|
+
if param.help is not None and isinstance(param.help, str):
|
|
147
|
+
translated = t(param.help)
|
|
148
|
+
if translated != param.help:
|
|
149
|
+
param.help = translated
|
|
150
|
+
|
|
151
|
+
if hasattr(node, "commands"):
|
|
152
|
+
for cmd in node.commands.values():
|
|
153
|
+
_translate_tree_helps(cmd)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def i18n_wrap_click_tree(root):
|
|
157
|
+
"""给 Click 命令树的每个节点注入 get_help 包装:求助时临时翻译 → 渲染 → 恢复。
|
|
158
|
+
|
|
159
|
+
不永久改写 Click 的 .help 属性——只在 get_help() 调用期间临时翻译,
|
|
160
|
+
finally 恢复原始 i18n key。这避免了「同一进程内语言被首次请求锁死」的问题。
|
|
161
|
+
|
|
162
|
+
解决 Typer/Click help 字符串是模块加载时求值、而语言要到运行时才确定的矛盾。
|
|
163
|
+
"""
|
|
164
|
+
from gbc.app.i18n import resolve_lang, set_lang
|
|
165
|
+
|
|
166
|
+
def _wrap_node(node):
|
|
167
|
+
orig_get_help = node.get_help
|
|
168
|
+
|
|
169
|
+
def translated_get_help(ctx):
|
|
170
|
+
# 语言可能已由 _main callback 设置;若未设置(例如根 --help 绕过了 callback)
|
|
171
|
+
# 则从 argv / env / locale 自判。
|
|
172
|
+
explicit = _lang_from_argv()
|
|
173
|
+
set_lang(resolve_lang(explicit))
|
|
174
|
+
|
|
175
|
+
# 保存 → 翻译 → 渲染 → 恢复(保证 finally 恢复,不泄漏)
|
|
176
|
+
snap = _snapshot_tree_helps(node)
|
|
177
|
+
try:
|
|
178
|
+
_translate_tree_helps(node)
|
|
179
|
+
return orig_get_help(ctx)
|
|
180
|
+
finally:
|
|
181
|
+
_restore_tree_helps(node, snap)
|
|
182
|
+
|
|
183
|
+
node.get_help = translated_get_help
|
|
184
|
+
|
|
185
|
+
if hasattr(node, "commands"):
|
|
186
|
+
for cmd in node.commands.values():
|
|
187
|
+
_wrap_node(cmd)
|
|
188
|
+
|
|
189
|
+
_wrap_node(root)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ======== Error Handling ========
|
|
193
|
+
|
|
194
|
+
def handle_error(e: Exception) -> typer.Exit:
|
|
195
|
+
"""统一异常处理,返回 typer.Exit 供 raise 使用。消息经 i18n 本地化。"""
|
|
196
|
+
from gbc.app.i18n import t
|
|
197
|
+
if isinstance(e, IllegalOperationError):
|
|
198
|
+
console.print(f"[bold red]{t('err.illegal_operation', msg=str(e))}[/bold red]")
|
|
199
|
+
elif isinstance(e, GuaranteeTestFailedError):
|
|
200
|
+
console.print(f"[bold red]{t('err.test_failed', guarantee=e.guarantee_path, target=e.target_file)}[/bold red]")
|
|
201
|
+
if e.failure_info:
|
|
202
|
+
console.print(f"[dim]{e.failure_info}[/dim]")
|
|
203
|
+
elif isinstance(e, GuaranteeHasDependentsError):
|
|
204
|
+
console.print(f"[bold red]{t('err.retire_blocked', msg=str(e))}[/bold red]")
|
|
205
|
+
elif isinstance(e, GuaranteeError):
|
|
206
|
+
console.print(f"[bold yellow]{t('err.guarantee', msg=str(e))}[/bold yellow]")
|
|
207
|
+
elif isinstance(e, ConfigError):
|
|
208
|
+
console.print(f"[bold red]{t('err.config', msg=str(e))}[/bold red]")
|
|
209
|
+
elif isinstance(e, ExecutorError):
|
|
210
|
+
console.print(f"[bold red]{t('err.executor', msg=str(e))}[/bold red]")
|
|
211
|
+
elif isinstance(e, GBCError):
|
|
212
|
+
console.print(f"[bold red]{t('err.generic', msg=str(e))}[/bold red]")
|
|
213
|
+
else:
|
|
214
|
+
console.print(f"[bold red]{t('err.unexpected', kind=type(e).__name__, msg=str(e))}[/bold red]")
|
|
215
|
+
return typer.Exit(code=1)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ======== Guarantee Commands ========
|
|
219
|
+
|
|
220
|
+
@guarantee_app.command("create")
|
|
221
|
+
def guarantee_create(
|
|
222
|
+
provider: str = typer.Argument(..., help="提供保证的源文件"),
|
|
223
|
+
id: str = typer.Argument(..., help="具名保证 id,如 config.llm.get_model.returns_loaded"),
|
|
224
|
+
test: str = typer.Argument(..., help="测试选择器(交给 executor 的 {file})"),
|
|
225
|
+
executor: str = typer.Argument(..., help="执行器配置名"),
|
|
226
|
+
desc: str = typer.Argument(..., help="保证描述"),
|
|
227
|
+
heavy: int = typer.Option(0, "--heavy", "-H", help="成本秩;>=1 批量跳过"),
|
|
228
|
+
timeout: int = typer.Option(-1, "--timeout", "-t", help="超时覆写,-1 用默认"),
|
|
229
|
+
disabled: bool = typer.Option(False, "--disabled", help="建成停用占位(跳过门禁)——仅用于打破循环依赖,事后须 enable"),
|
|
230
|
+
):
|
|
231
|
+
"""新建一条保证。出生即绿:当场跑测试,不过则拒绝。--disabled 则跳过门禁建占位。"""
|
|
232
|
+
try:
|
|
233
|
+
base.create_guarantee(provider, id, desc, test, executor, heavy, timeout, disabled)
|
|
234
|
+
if disabled:
|
|
235
|
+
console.print(f"[yellow]✔[/yellow] Created [bold]{id}[/bold] [DISABLED placeholder] — enable it once the test passes")
|
|
236
|
+
else:
|
|
237
|
+
console.print(f"[green]✔[/green] Created [bold]{id}[/bold] on [bold]{provider}[/bold]")
|
|
238
|
+
except Exception as e:
|
|
239
|
+
raise handle_error(e)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@guarantee_app.command("update")
|
|
243
|
+
def guarantee_update(
|
|
244
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
245
|
+
id: str = typer.Argument(..., help="保证 id"),
|
|
246
|
+
desc: Optional[str] = typer.Option(None, "--desc", help="新描述"),
|
|
247
|
+
test: Optional[str] = typer.Option(None, "--test", help="新测试选择器"),
|
|
248
|
+
executor: Optional[str] = typer.Option(None, "--executor", help="新执行器"),
|
|
249
|
+
heavy: Optional[int] = typer.Option(None, "--heavy", "-H", help="新成本秩"),
|
|
250
|
+
timeout: Optional[int] = typer.Option(None, "--timeout", "-t", help="新超时覆写"),
|
|
251
|
+
):
|
|
252
|
+
"""更新保证字段。改了测试/执行方式会重新跑门禁。"""
|
|
253
|
+
try:
|
|
254
|
+
base.update_guarantee(
|
|
255
|
+
provider, id, desc=desc, test=test, executor_name=executor,
|
|
256
|
+
heavy=heavy, timeout_override=timeout,
|
|
257
|
+
)
|
|
258
|
+
console.print(f"[green]✔[/green] Updated [bold]{id}[/bold]")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
raise handle_error(e)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@guarantee_app.command("retire")
|
|
264
|
+
def guarantee_retire(
|
|
265
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
266
|
+
id: str = typer.Argument(..., help="要退休的保证 id"),
|
|
267
|
+
):
|
|
268
|
+
"""退休一条保证。仍有 dependents 则拒绝(退休保护)。"""
|
|
269
|
+
try:
|
|
270
|
+
base.retire_guarantee(provider, id)
|
|
271
|
+
console.print(f"[yellow]✔[/yellow] Retired [bold]{id}[/bold]")
|
|
272
|
+
except Exception as e:
|
|
273
|
+
raise handle_error(e)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@guarantee_app.command("disable")
|
|
277
|
+
def guarantee_disable(
|
|
278
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
279
|
+
id: str = typer.Argument(..., help="要停用的保证 id"),
|
|
280
|
+
):
|
|
281
|
+
"""停用一条保证:保留 id 与全部边,暂缓门禁/批量 verify。停用 ≠ 退休,不删任何东西。"""
|
|
282
|
+
try:
|
|
283
|
+
base.disable_guarantee(provider, id)
|
|
284
|
+
console.print(f"[yellow]✔[/yellow] Disabled [bold]{id}[/bold] — edges kept; enable to re-prove")
|
|
285
|
+
except Exception as e:
|
|
286
|
+
raise handle_error(e)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@guarantee_app.command("enable")
|
|
290
|
+
def guarantee_enable(
|
|
291
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
292
|
+
id: str = typer.Argument(..., help="要恢复的保证 id"),
|
|
293
|
+
):
|
|
294
|
+
"""恢复一条停用保证:当场补跑门禁(born-green),过了才转正;不过则保持停用。"""
|
|
295
|
+
try:
|
|
296
|
+
base.enable_guarantee(provider, id)
|
|
297
|
+
console.print(f"[green]✔[/green] Enabled [bold]{id}[/bold]")
|
|
298
|
+
except Exception as e:
|
|
299
|
+
raise handle_error(e)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@guarantee_app.command("list")
|
|
303
|
+
def guarantee_list(
|
|
304
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
305
|
+
):
|
|
306
|
+
"""列出 provider 提供的所有保证及其 dependents。"""
|
|
307
|
+
try:
|
|
308
|
+
data = base.list_provides(provider)
|
|
309
|
+
if not data:
|
|
310
|
+
console.print("[dim]No guarantees.[/dim]")
|
|
311
|
+
return
|
|
312
|
+
table = Table(title=f"Provides → {provider}")
|
|
313
|
+
table.add_column("id", style="cyan", no_wrap=True)
|
|
314
|
+
table.add_column("state", justify="center")
|
|
315
|
+
table.add_column("heavy", justify="center")
|
|
316
|
+
table.add_column("dependents", justify="center")
|
|
317
|
+
table.add_column("desc", style="white")
|
|
318
|
+
for gid, g in data.items():
|
|
319
|
+
state = "[magenta]DISABLED[/magenta]" if g.disabled else "[green]on[/green]"
|
|
320
|
+
table.add_row(gid, state, str(g.heavy), str(len(g.dependents)), g.desc)
|
|
321
|
+
console.print(table)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
raise handle_error(e)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ======== Dependency Commands ========
|
|
327
|
+
|
|
328
|
+
@dep_app.command("add")
|
|
329
|
+
def dep_add(
|
|
330
|
+
consumer: str = typer.Argument(..., help="依赖方文件"),
|
|
331
|
+
provider: str = typer.Argument(..., help="被依赖的源文件"),
|
|
332
|
+
symbol: str = typer.Argument(..., help="provider 上的符号名"),
|
|
333
|
+
guarantee: Optional[str] = typer.Option(None, "--guarantee", "-g", help="挂的保证 id;不给=免费 symbol 依赖"),
|
|
334
|
+
):
|
|
335
|
+
"""登记 consumer 对 provider 的依赖(行为级会双向写)。"""
|
|
336
|
+
try:
|
|
337
|
+
base.add_dependency(consumer, provider, symbol, guarantee)
|
|
338
|
+
console.print(f"[green]✔[/green] {consumer} → {provider}:{symbol}" + (f" [{guarantee}]" if guarantee else " (free)"))
|
|
339
|
+
except Exception as e:
|
|
340
|
+
raise handle_error(e)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dep_app.command("remove")
|
|
344
|
+
def dep_remove(
|
|
345
|
+
consumer: str = typer.Argument(..., help="依赖方文件"),
|
|
346
|
+
provider: str = typer.Argument(..., help="被依赖的源文件"),
|
|
347
|
+
symbol: str = typer.Argument(..., help="provider 上的符号名"),
|
|
348
|
+
guarantee: Optional[str] = typer.Option(None, "--guarantee", "-g", help="只摘这个保证;不给=撤整条 symbol 边"),
|
|
349
|
+
):
|
|
350
|
+
"""撤销依赖边(维护双向一致)。"""
|
|
351
|
+
try:
|
|
352
|
+
base.remove_dependency(consumer, provider, symbol, guarantee)
|
|
353
|
+
console.print(f"[yellow]✔[/yellow] removed {consumer} → {provider}:{symbol}")
|
|
354
|
+
except Exception as e:
|
|
355
|
+
raise handle_error(e)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dep_app.command("of")
|
|
359
|
+
def dep_of(
|
|
360
|
+
consumer: str = typer.Argument(..., help="依赖方文件"),
|
|
361
|
+
):
|
|
362
|
+
"""列出某文件声明的全部依赖边。"""
|
|
363
|
+
try:
|
|
364
|
+
deps = base.list_depends_on(consumer)
|
|
365
|
+
if not deps:
|
|
366
|
+
console.print("[dim]No dependencies.[/dim]")
|
|
367
|
+
return
|
|
368
|
+
table = Table(title=f"Depends on ← {consumer}")
|
|
369
|
+
table.add_column("symbol", style="cyan")
|
|
370
|
+
table.add_column("guarantees", style="white")
|
|
371
|
+
for d in deps:
|
|
372
|
+
table.add_row(d.symbol, ", ".join(d.guarantees) if d.guarantees else "[dim](free)[/dim]")
|
|
373
|
+
console.print(table)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
raise handle_error(e)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@dep_app.command("who")
|
|
379
|
+
def dep_who(
|
|
380
|
+
provider: str = typer.Argument(..., help="被依赖的源文件"),
|
|
381
|
+
symbol: Optional[str] = typer.Option(None, "--symbol", "-s", help="收窄到某个符号"),
|
|
382
|
+
guarantee: Optional[str] = typer.Option(None, "--guarantee", "-g", help="某保证 id(走 O(1) 反向边)"),
|
|
383
|
+
):
|
|
384
|
+
"""反查谁依赖 provider(取代手工 grep)。"""
|
|
385
|
+
try:
|
|
386
|
+
result = base.who_depends_on(provider, symbol=symbol, guarantee_id=guarantee)
|
|
387
|
+
console.print_json(json.dumps(result, ensure_ascii=False))
|
|
388
|
+
except Exception as e:
|
|
389
|
+
raise handle_error(e)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ======== Verify Commands ========
|
|
393
|
+
|
|
394
|
+
@verify_app.command("provider")
|
|
395
|
+
def verify_provider(
|
|
396
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
397
|
+
max_heavy: int = typer.Option(0, "--max-heavy", "-H", help="批量只跑 heavy <= 该值"),
|
|
398
|
+
timeout: int = typer.Option(-1, "--timeout", "-t", help="超时覆写"),
|
|
399
|
+
):
|
|
400
|
+
"""验证 provider 的所有保证,按 heavy 阈值跳过并三桶汇总。"""
|
|
401
|
+
try:
|
|
402
|
+
s = base.verify_provider(provider, auto_run_max_heavy=max_heavy, timeout=timeout)
|
|
403
|
+
light = "[green]GREEN[/green]" if s.green else "[red]RED[/red]"
|
|
404
|
+
console.print(f"{light} passed={len(s.passed)} failed={len(s.failed)} skipped={len(s.skipped)}")
|
|
405
|
+
if s.failed:
|
|
406
|
+
console.print(f"[red]failed:[/red] {', '.join(s.failed)}")
|
|
407
|
+
disabled = [sk for sk in s.skipped if sk.reason == "disabled"]
|
|
408
|
+
heavy_sk = [sk for sk in s.skipped if sk.reason != "disabled"]
|
|
409
|
+
if heavy_sk:
|
|
410
|
+
tags = ", ".join(f"{sk.id}(heavy={sk.heavy})" for sk in heavy_sk)
|
|
411
|
+
console.print(f"[yellow]{len(heavy_sk)} heavy skipped:[/yellow] {tags}")
|
|
412
|
+
if disabled:
|
|
413
|
+
tags = ", ".join(sk.id for sk in disabled)
|
|
414
|
+
console.print(f"[magenta]{len(disabled)} DISABLED (born-green suspended):[/magenta] {tags}")
|
|
415
|
+
except Exception as e:
|
|
416
|
+
raise handle_error(e)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@verify_app.command("single")
|
|
420
|
+
def verify_single(
|
|
421
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
422
|
+
id: str = typer.Argument(..., help="保证 id"),
|
|
423
|
+
timeout: int = typer.Option(-1, "--timeout", "-t", help="超时覆写"),
|
|
424
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="显示完整 stdout/stderr"),
|
|
425
|
+
):
|
|
426
|
+
"""点名验证单条保证——无视 heavy,永远跑。"""
|
|
427
|
+
try:
|
|
428
|
+
result = base.verify_guarantee(provider, id, timeout=timeout)
|
|
429
|
+
passed = result.return_code == 0
|
|
430
|
+
status = "[green]PASS[/green]" if passed else "[red]FAIL[/red]"
|
|
431
|
+
console.print(f"{status} {id} (exit {result.return_code})")
|
|
432
|
+
if verbose or not passed:
|
|
433
|
+
if result.stdout:
|
|
434
|
+
console.print(f"[dim]── stdout ──[/dim]\n{result.stdout.rstrip()}")
|
|
435
|
+
if result.stderr:
|
|
436
|
+
console.print(f"[dim]── stderr ──[/dim]\n{result.stderr.rstrip()}")
|
|
437
|
+
except Exception as e:
|
|
438
|
+
raise handle_error(e)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ======== Refactor ========
|
|
442
|
+
|
|
443
|
+
refactor_app = typer.Typer(help="cli.refactor.help")
|
|
444
|
+
app.add_typer(refactor_app, name="refactor")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@refactor_app.command("file")
|
|
448
|
+
def refactor_file_cmd(
|
|
449
|
+
old: str = typer.Argument(..., help="当前路径(文件或目录)"),
|
|
450
|
+
new: str = typer.Argument(..., help="目标路径"),
|
|
451
|
+
no_disable: bool = typer.Option(False, "--no-disable", help="不自动停用被移动方的保证(默认会停用)"),
|
|
452
|
+
):
|
|
453
|
+
"""移动文件/目录 + 它的 .gbc 产物,全图重写路径引用,并自动停用被移动方的保证。
|
|
454
|
+
|
|
455
|
+
id 不动(路径无关)。移动是幂等的:已手动搬走则只收尾图引用。改完逐个 enable。
|
|
456
|
+
"""
|
|
457
|
+
try:
|
|
458
|
+
report = base.refactor_file(old, new, disable_guarantees=not no_disable)
|
|
459
|
+
console.print(f"[green]✔[/green] {report['old']} → {report['new']}")
|
|
460
|
+
console.print(
|
|
461
|
+
f" code={report['code_move']} gbc={report['gbc_move']} "
|
|
462
|
+
f"refs_rewritten={report['refs_rewritten']} md_refs={report['md_refs_rewritten']} "
|
|
463
|
+
f"disabled={len(report['disabled'])}"
|
|
464
|
+
)
|
|
465
|
+
if report["disabled"]:
|
|
466
|
+
ids = ", ".join(d["guarantee"] for d in report["disabled"])
|
|
467
|
+
console.print(f"[magenta]disabled (enable after fixing tests):[/magenta] {ids}")
|
|
468
|
+
console.print(f"[dim]{report['next_steps']}[/dim]")
|
|
469
|
+
except Exception as e:
|
|
470
|
+
raise handle_error(e)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@refactor_app.command("rename-id")
|
|
474
|
+
def refactor_rename_id_cmd(
|
|
475
|
+
provider: str = typer.Argument(..., help="提供保证的源文件"),
|
|
476
|
+
old_id: str = typer.Argument(..., help="当前保证 id"),
|
|
477
|
+
new_id: str = typer.Argument(..., help="新保证 id"),
|
|
478
|
+
):
|
|
479
|
+
"""保证 id 改名(双向同步消费者)。用于把带路径前缀的旧 id 归一成 <symbol>.<behavior>。"""
|
|
480
|
+
try:
|
|
481
|
+
rep = base.rename_guarantee(provider, old_id, new_id)
|
|
482
|
+
console.print(f"[green]✔[/green] {rep['old_id']} → {rep['new_id']} (consumers: {len(rep['consumers_updated'])})")
|
|
483
|
+
except Exception as e:
|
|
484
|
+
raise handle_error(e)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@refactor_app.command("func")
|
|
488
|
+
def refactor_func_cmd(
|
|
489
|
+
provider: str = typer.Argument(..., help="源文件"),
|
|
490
|
+
old_symbol: str = typer.Argument(..., help="当前符号名"),
|
|
491
|
+
new_symbol: str = typer.Argument(..., help="新符号名"),
|
|
492
|
+
no_disable: bool = typer.Option(False, "--no-disable", help="不自动停用受影响保证"),
|
|
493
|
+
):
|
|
494
|
+
"""符号改名:改消费者 symbol 字段 + 该符号名下的保证 id,自动停用。源码 def/调用处由 AI 改。"""
|
|
495
|
+
try:
|
|
496
|
+
rep = base.refactor_func(provider, old_symbol, new_symbol, disable_guarantees=not no_disable)
|
|
497
|
+
console.print(f"[green]✔[/green] {provider}:{rep['old_symbol']} → {rep['new_symbol']}")
|
|
498
|
+
console.print(f" symbol_refs={rep['symbol_refs_rewritten']} md_refs={rep['md_refs_rewritten']} ids_renamed={len(rep['ids_renamed'])} disabled={len(rep['disabled'])}")
|
|
499
|
+
console.print(f"[dim]{rep['next_steps']}[/dim]")
|
|
500
|
+
except Exception as e:
|
|
501
|
+
raise handle_error(e)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ======== Tree ========
|
|
505
|
+
|
|
506
|
+
@app.command("tree", help="cli.tree.help")
|
|
507
|
+
def tree_cmd(
|
|
508
|
+
detail: bool = typer.Option(False, "--detail", "-d", help="cli.tree.option.detail"),
|
|
509
|
+
gaps: bool = typer.Option(False, "--gaps", "-g", help="cli.tree.option.gaps"),
|
|
510
|
+
):
|
|
511
|
+
"""把整棵 .gbc 渲染成一份 AI 可读的依赖树(gbc.md 意图为骨 + json 依赖边)。"""
|
|
512
|
+
try:
|
|
513
|
+
# 用 print 而非 console.print:树里有 [意图]/[保证] 等方括号,避免被 rich 当样式标记解析。
|
|
514
|
+
print(base.render_tree(detail=detail, gaps=gaps))
|
|
515
|
+
except Exception as e:
|
|
516
|
+
raise handle_error(e)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ======== Doctor ========
|
|
520
|
+
|
|
521
|
+
@doctor_app.command("check")
|
|
522
|
+
def doctor_check():
|
|
523
|
+
"""全局一致性体检:悬空引用 + 双向边漂移 + 停用保证(响亮报出)。"""
|
|
524
|
+
try:
|
|
525
|
+
violations = base.check_consistency()
|
|
526
|
+
if not violations:
|
|
527
|
+
console.print("[green]✔ consistent[/green]")
|
|
528
|
+
return
|
|
529
|
+
disabled_types = {"disabled_guarantee", "depends_on_disabled"}
|
|
530
|
+
errors = [v for v in violations if v["type"] not in disabled_types]
|
|
531
|
+
notices = [v for v in violations if v["type"] in disabled_types]
|
|
532
|
+
if errors:
|
|
533
|
+
console.print(f"[red]✘ {len(errors)} error(s):[/red]")
|
|
534
|
+
console.print_json(json.dumps(errors, ensure_ascii=False))
|
|
535
|
+
if notices:
|
|
536
|
+
console.print(f"[magenta]⊘ {len(notices)} disabled notice(s) (not errors, but loud until enabled):[/magenta]")
|
|
537
|
+
console.print_json(json.dumps(notices, ensure_ascii=False))
|
|
538
|
+
except Exception as e:
|
|
539
|
+
raise handle_error(e)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ======== Executor Commands ========
|
|
543
|
+
|
|
544
|
+
@executor_app.command("upsert")
|
|
545
|
+
def executor_upsert(
|
|
546
|
+
config_name: str = typer.Argument(..., help="执行器配置名称"),
|
|
547
|
+
config_json: Optional[str] = typer.Option(None, "--json", "-j", help="JSON 字符串"),
|
|
548
|
+
config_file: Optional[Path] = typer.Option(None, "--file", "-f", help="JSON 文件路径"),
|
|
549
|
+
):
|
|
550
|
+
"""更新或插入一个执行器配置。通过 --json 或 --file 提供配置数据。"""
|
|
551
|
+
data = _parse_executor_input(config_json, config_file)
|
|
552
|
+
try:
|
|
553
|
+
base.upsert_executor(config_name, data)
|
|
554
|
+
console.print(f"[green]✔[/green] Executor [bold]{config_name}[/bold] configured.")
|
|
555
|
+
except Exception as e:
|
|
556
|
+
raise handle_error(e)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _parse_executor_input(config_json: Optional[str], config_file: Optional[Path]) -> dict:
|
|
560
|
+
"""解析执行器配置输入,返回 dict。失败则直接退出。"""
|
|
561
|
+
if config_file:
|
|
562
|
+
if not config_file.exists():
|
|
563
|
+
console.print(f"[red]Error: file not found: {config_file}[/red]")
|
|
564
|
+
raise typer.Exit(code=1)
|
|
565
|
+
try:
|
|
566
|
+
return json.loads(config_file.read_text(encoding="utf-8"))
|
|
567
|
+
except json.JSONDecodeError:
|
|
568
|
+
console.print(f"[red]Error: invalid JSON in file: {config_file}[/red]")
|
|
569
|
+
raise typer.Exit(code=1)
|
|
570
|
+
|
|
571
|
+
if config_json:
|
|
572
|
+
try:
|
|
573
|
+
return json.loads(config_json)
|
|
574
|
+
except json.JSONDecodeError:
|
|
575
|
+
console.print("[red]Error: invalid JSON string[/red]")
|
|
576
|
+
raise typer.Exit(code=1)
|
|
577
|
+
|
|
578
|
+
console.print("[yellow]Please provide --json or --file[/yellow]")
|
|
579
|
+
raise typer.Exit(code=1)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# ======== Entry Point ========
|
|
583
|
+
|
|
584
|
+
if __name__ == "__main__":
|
|
585
|
+
app()
|