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/mcp.py
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
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
|
+
"""MCP 工具面:把 base 层的能力暴露成 agent 可调用的工具。
|
|
16
|
+
|
|
17
|
+
设计取向(相对旧版 mcp):
|
|
18
|
+
- 保证为一等公民、具名 id;依赖支持「多消费者共享同一保证」。
|
|
19
|
+
- 依赖登记是双向写(consumer.depends_on ⇄ provider.dependents),由工具兜底,
|
|
20
|
+
agent 不必手工在两处读写。
|
|
21
|
+
- 退休保护:retire_guarantee 对仍有 dependents 的保证直接拒绝——把「别删还有人
|
|
22
|
+
依赖的保证」从靠自觉变成机制兜底。
|
|
23
|
+
- 反查与体检:who_depends_on / check_consistency 取代手工 grep。
|
|
24
|
+
|
|
25
|
+
所有工具返回 JSON 字符串;出错统一返回 {"error": ...},不抛异常给 MCP 运行时。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
from mcp.server.fastmcp import FastMCP
|
|
31
|
+
|
|
32
|
+
from . import base
|
|
33
|
+
from gbc.app.intent import base as intent_base
|
|
34
|
+
from gbc.app.models.errors import GBCError
|
|
35
|
+
|
|
36
|
+
mcp = FastMCP("gbc")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ok(payload) -> str:
|
|
40
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _err(e: Exception) -> str:
|
|
44
|
+
if isinstance(e, GBCError):
|
|
45
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
46
|
+
return json.dumps({"error": f"Unexpected error: {e}"}, ensure_ascii=False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ======== 保证生命周期(provider 侧) ========
|
|
50
|
+
|
|
51
|
+
@mcp.tool()
|
|
52
|
+
def create_guarantee(
|
|
53
|
+
provider: str,
|
|
54
|
+
id: str,
|
|
55
|
+
desc: str,
|
|
56
|
+
test: str,
|
|
57
|
+
executor: str,
|
|
58
|
+
heavy: int = 0,
|
|
59
|
+
timeout_override: int = -1,
|
|
60
|
+
disabled: bool = False,
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Create a new named guarantee on a provider file. Born-green: the test is run
|
|
63
|
+
immediately and creation is rejected if it fails.
|
|
64
|
+
|
|
65
|
+
Id convention: `<symbol>.<behavior>` (e.g. "make_game.returns_html", "store.roundtrip").
|
|
66
|
+
Do NOT encode the provider path in the id — the path is already carried by the
|
|
67
|
+
provider arg here and by the consumer's symbol field. Ids need only be unique
|
|
68
|
+
PER provider, not globally (every lookup is keyed by (provider, id)).
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
provider: Source file that provides this guarantee (e.g. "app/config/llm.py")
|
|
72
|
+
id: Semantic guarantee id `<symbol>.<behavior>`, e.g. "get_model.returns_loaded" (identity, path-free)
|
|
73
|
+
desc: What behavior is promised and why it matters
|
|
74
|
+
test: Test selector passed to the executor ({file} substitution), e.g. "tests/test_x.py::test_y"
|
|
75
|
+
executor: Executor config name that knows how to run the test
|
|
76
|
+
heavy: Cost rank; 0 runs in batch, >=1 is skipped in batch verify (and reported)
|
|
77
|
+
timeout_override: Per-guarantee timeout in seconds; -1 uses executor default
|
|
78
|
+
disabled: Create as a DISABLED placeholder, SKIPPING born-green — only for breaking
|
|
79
|
+
circular dependencies (register the id+edge before its test can pass yet). The
|
|
80
|
+
disabled guarantee is surfaced loudly by check_consistency until you enable it.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
base.create_guarantee(provider, id, desc, test, executor, heavy, timeout_override, disabled)
|
|
84
|
+
return "created" if not disabled else "created (disabled placeholder — enable it once the test passes)"
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return _err(e)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
def disable_guarantee(provider: str, id: str) -> str:
|
|
91
|
+
"""Temporarily disable a guarantee: its id and all edges (dependents/reverse edges)
|
|
92
|
+
are kept intact, but born-green and batch verify are SUSPENDED for it (not run, not
|
|
93
|
+
failed — reported as skipped/disabled). Disable is NOT retire: it deletes nothing and
|
|
94
|
+
touches no dependency. Use it to hold an edge across a refactor window or while a test
|
|
95
|
+
is under repair: disable → fix the test → enable to re-prove born-green.
|
|
96
|
+
|
|
97
|
+
A disabled guarantee is a hole in the born-green wall, so it stays LOUD: check_consistency
|
|
98
|
+
reports it (and anything depending on it) until you enable it back.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
provider: Source file path
|
|
102
|
+
id: Guarantee id to disable
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
base.disable_guarantee(provider, id)
|
|
106
|
+
return "disabled"
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return _err(e)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@mcp.tool()
|
|
112
|
+
def enable_guarantee(provider: str, id: str) -> str:
|
|
113
|
+
"""Re-enable a disabled guarantee: born-green is re-run NOW, and disabled is cleared
|
|
114
|
+
only if the test passes. If it fails, the guarantee STAYS disabled (enable is refused) —
|
|
115
|
+
a guarantee is never silently promoted back without proof.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
provider: Source file path
|
|
119
|
+
id: Guarantee id to enable
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
base.enable_guarantee(provider, id)
|
|
123
|
+
return "enabled"
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return _err(e)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@mcp.tool()
|
|
129
|
+
def update_guarantee(
|
|
130
|
+
provider: str,
|
|
131
|
+
id: str,
|
|
132
|
+
desc: str | None = None,
|
|
133
|
+
test: str | None = None,
|
|
134
|
+
executor: str | None = None,
|
|
135
|
+
heavy: int | None = None,
|
|
136
|
+
timeout_override: int | None = None,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Update fields of an existing guarantee. Only pass what you want to change.
|
|
139
|
+
If the test/executor/timeout changes, the test is re-run (born-green is re-proven).
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
provider: Source file path
|
|
143
|
+
id: Guarantee id to update
|
|
144
|
+
desc/test/executor/heavy/timeout_override: New values; omit to leave unchanged
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
base.update_guarantee(
|
|
148
|
+
provider, id,
|
|
149
|
+
desc=desc, test=test, executor_name=executor,
|
|
150
|
+
heavy=heavy, timeout_override=timeout_override,
|
|
151
|
+
)
|
|
152
|
+
return "updated"
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return _err(e)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
def retire_guarantee(provider: str, id: str) -> str:
|
|
159
|
+
"""Retire (delete) a guarantee. REFUSED if it still has dependents — repair or
|
|
160
|
+
migrate the dependents first. This guards against silently breaking downstream code.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
provider: Source file path
|
|
164
|
+
id: Guarantee id to retire
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
base.retire_guarantee(provider, id)
|
|
168
|
+
return "retired"
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return _err(e)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ======== 依赖边(consumer 侧,双向写) ========
|
|
174
|
+
|
|
175
|
+
@mcp.tool()
|
|
176
|
+
def add_dependency(provider: str, consumer: str, symbol: str, guarantee_id: str | None = None) -> str:
|
|
177
|
+
"""Register that `consumer` depends on a `symbol` of `provider`.
|
|
178
|
+
|
|
179
|
+
- guarantee_id omitted: a FREE symbol-level dependency (depends on the symbol
|
|
180
|
+
existing / its signature, not on any specific behavior; no test, no reverse edge).
|
|
181
|
+
- guarantee_id given: a BEHAVIOR dependency. The guarantee must already exist on the
|
|
182
|
+
provider (create it first if not). The reverse edge (provider's dependents) is
|
|
183
|
+
written automatically. Multiple consumers may share one guarantee.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
provider: Source file providing the symbol
|
|
187
|
+
consumer: File that depends on it
|
|
188
|
+
symbol: Symbol name on the provider (e.g. "get_model"); stored as "<provider>:<symbol>"
|
|
189
|
+
guarantee_id: Existing guarantee id to attach, or omit for a free symbol dependency
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
base.add_dependency(consumer, provider, symbol, guarantee_id)
|
|
193
|
+
return "added"
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return _err(e)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@mcp.tool()
|
|
199
|
+
def remove_dependency(provider: str, consumer: str, symbol: str, guarantee_id: str | None = None) -> str:
|
|
200
|
+
"""Remove a dependency edge (inverse of add_dependency; keeps both directions in sync).
|
|
201
|
+
|
|
202
|
+
- guarantee_id given: detach only that guarantee from the edge.
|
|
203
|
+
- guarantee_id omitted: remove the whole symbol edge and detach the consumer from
|
|
204
|
+
every guarantee it carried.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
provider: Source file path
|
|
208
|
+
consumer: Dependent file path
|
|
209
|
+
symbol: Symbol name on the provider
|
|
210
|
+
guarantee_id: Specific guarantee to detach, or omit to remove the whole edge
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
base.remove_dependency(consumer, provider, symbol, guarantee_id)
|
|
214
|
+
return "removed"
|
|
215
|
+
except Exception as e:
|
|
216
|
+
return _err(e)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ======== Refactor / 重定位 ========
|
|
220
|
+
|
|
221
|
+
@mcp.tool()
|
|
222
|
+
def refactor_file(old: str, new: str, disable_guarantees: bool = True) -> str:
|
|
223
|
+
"""Relocate a file (or a whole directory subtree) and fix EVERY graph reference to it
|
|
224
|
+
in one shot — the move primitive the dependency graph lacked.
|
|
225
|
+
|
|
226
|
+
GBC does the structural + metadata part: moves the code file/dir + its .gbc artifacts
|
|
227
|
+
(json/.pyi) with `git mv` (history preserved), rewrites all path references graph-wide
|
|
228
|
+
(consumers' symbol path-prefix, providers' dependents entries), and auto-disables the
|
|
229
|
+
guarantees the moved file provides (their tests break on stale imports until fixed).
|
|
230
|
+
Guarantee IDS ARE NOT TOUCHED — ids are path-free (`<symbol>.<behavior>`), so a move
|
|
231
|
+
never changes them. To rename ids/symbols, use refactor_func.
|
|
232
|
+
|
|
233
|
+
Then YOU (the agent) do the content + verification part: fix imports in the moved file
|
|
234
|
+
and its consumers, move/rename test files and `update_guarantee(test=...)` their
|
|
235
|
+
selectors, then `enable_guarantee` each disabled id (born-green re-runs at the new path).
|
|
236
|
+
|
|
237
|
+
The move is idempotent: if the file was already moved by hand (old gone, new present),
|
|
238
|
+
GBC skips the move and just reconciles the stale graph references — so this also cleans
|
|
239
|
+
up a half-finished manual relocation.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
old: Current path of the file or directory (project-relative)
|
|
243
|
+
new: Destination path
|
|
244
|
+
disable_guarantees: Auto-disable guarantees under the moved path (default True; set
|
|
245
|
+
False only if you know the tests still pass as-is)
|
|
246
|
+
|
|
247
|
+
Returns: JSON report {old, new, code_move, gbc_move, refs_rewritten, disabled, next_steps}
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
return _ok(base.refactor_file(old, new, disable_guarantees=disable_guarantees))
|
|
251
|
+
except Exception as e:
|
|
252
|
+
return _err(e)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@mcp.tool()
|
|
256
|
+
def rename_guarantee(provider: str, old_id: str, new_id: str) -> str:
|
|
257
|
+
"""Rename a guarantee id (old_id -> new_id), keeping both directions consistent.
|
|
258
|
+
|
|
259
|
+
The id lives in two places — the provider's provides key and every dependent consumer's
|
|
260
|
+
`guarantees` list. This rewrites both: the provider re-keys the guarantee (its object —
|
|
261
|
+
disabled flag, dependents, test — is preserved), then every consumer that depends on it
|
|
262
|
+
gets old_id swapped to new_id. Pure id rename: touches no test, no symbol, no path.
|
|
263
|
+
|
|
264
|
+
Use it to normalize legacy path-prefixed ids into path-free `<symbol>.<behavior>` form
|
|
265
|
+
(e.g. "core.maker.make_game.returns_html" -> "make_game.returns_html").
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
provider: Source file that provides the guarantee
|
|
269
|
+
old_id: Current guarantee id
|
|
270
|
+
new_id: New guarantee id (must be free on this provider)
|
|
271
|
+
|
|
272
|
+
Returns: JSON {provider, old_id, new_id, consumers_updated}
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
return _ok(base.rename_guarantee(provider, old_id, new_id))
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return _err(e)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@mcp.tool()
|
|
281
|
+
def refactor_func(provider: str, old_symbol: str, new_symbol: str, disable_guarantees: bool = True) -> str:
|
|
282
|
+
"""Rename a symbol on a provider (old_symbol -> new_symbol) and fix every graph reference.
|
|
283
|
+
|
|
284
|
+
GBC does the metadata part: rewrites consumers' `provider:old_symbol` dependency symbols to
|
|
285
|
+
`provider:new_symbol`, renames the guarantee ids under that symbol (`old_symbol` / `old_symbol.*`
|
|
286
|
+
-> `new_symbol[...]`, per the `<symbol>.<behavior>` convention, both directions), and auto-disables
|
|
287
|
+
those guarantees (their tests still call the old name and would fail).
|
|
288
|
+
|
|
289
|
+
GBC does NOT edit the symbol definition in source (that's an AST-level content edit). YOU rename
|
|
290
|
+
`def old_symbol` and its call sites + tests, then `enable_guarantee` each renamed id. Paths are
|
|
291
|
+
untouched; only the symbol segment of the id changes.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
provider: Source file path
|
|
295
|
+
old_symbol: Current symbol name (e.g. "make_game")
|
|
296
|
+
new_symbol: New symbol name
|
|
297
|
+
disable_guarantees: Auto-disable affected guarantees (default True)
|
|
298
|
+
|
|
299
|
+
Returns: JSON {provider, old_symbol, new_symbol, symbol_refs_rewritten, ids_renamed, disabled, next_steps}
|
|
300
|
+
"""
|
|
301
|
+
try:
|
|
302
|
+
return _ok(base.refactor_func(provider, old_symbol, new_symbol, disable_guarantees=disable_guarantees))
|
|
303
|
+
except Exception as e:
|
|
304
|
+
return _err(e)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ======== 读 / 反查 ========
|
|
308
|
+
|
|
309
|
+
@mcp.tool()
|
|
310
|
+
def list_provides(provider: str) -> str:
|
|
311
|
+
"""List all guarantees a provider offers, each with its dependents.
|
|
312
|
+
|
|
313
|
+
Returns: JSON object mapping guarantee id -> guarantee object
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
result = base.list_provides(provider)
|
|
317
|
+
return _ok({gid: g.model_dump() for gid, g in result.items()})
|
|
318
|
+
except Exception as e:
|
|
319
|
+
return _err(e)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@mcp.tool()
|
|
323
|
+
def list_depends_on(consumer: str) -> str:
|
|
324
|
+
"""List all dependency edges a file declares (what it depends on).
|
|
325
|
+
|
|
326
|
+
Returns: JSON array of dependency objects ({symbol, guarantees})
|
|
327
|
+
"""
|
|
328
|
+
try:
|
|
329
|
+
result = base.list_depends_on(consumer)
|
|
330
|
+
return _ok([d.model_dump() for d in result])
|
|
331
|
+
except Exception as e:
|
|
332
|
+
return _err(e)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@mcp.tool()
|
|
336
|
+
def who_depends_on(provider: str, symbol: str | None = None, guarantee_id: str | None = None) -> str:
|
|
337
|
+
"""Reverse lookup: who depends on this provider. Replaces ad-hoc grep.
|
|
338
|
+
|
|
339
|
+
- guarantee_id given: O(1) read of that guarantee's dependents.
|
|
340
|
+
- otherwise: global scan of every file's dependency edges pointing at this provider
|
|
341
|
+
(optionally filtered to a single symbol). This is the only way to find free
|
|
342
|
+
symbol-level dependents (they have no reverse edge).
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
provider: Source file path
|
|
346
|
+
symbol: Optional symbol name to narrow the scan
|
|
347
|
+
guarantee_id: Optional guarantee id for the fast O(1) path
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
return _ok(base.who_depends_on(provider, symbol=symbol, guarantee_id=guarantee_id))
|
|
351
|
+
except Exception as e:
|
|
352
|
+
return _err(e)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@mcp.tool()
|
|
356
|
+
def tree(detail: bool = False, gaps: bool = False) -> str:
|
|
357
|
+
"""Render the whole `.gbc` tree as one AI-readable dependency document.
|
|
358
|
+
|
|
359
|
+
Backbone = every folder's intent / internal constraints / file entries (from
|
|
360
|
+
gbc.md); each file leaf is annotated with its dependency edges (→ provider:symbol
|
|
361
|
+
[guarantee]) and the guarantees it provides (⊕ guarantee ← dependents). A single
|
|
362
|
+
read-only call that replaces opening many gbc.md/json files to grasp the architecture.
|
|
363
|
+
|
|
364
|
+
detail: also expand each guarantee's desc/test/heavy, and list other artifacts
|
|
365
|
+
(.pyi stubs) present in each .gbc folder (absence => interface not materialized).
|
|
366
|
+
gaps: append a "registration gaps" section — purely graph-derived (no filesystem
|
|
367
|
+
scan, zero false positives on config/assets): files that have .gbc json or
|
|
368
|
+
are depended-upon yet have no gbc.md file entry.
|
|
369
|
+
"""
|
|
370
|
+
try:
|
|
371
|
+
# 文档型工具:直接返回原始文本(不经 _ok 的 json.dumps),让 MCP 当文本块发出,
|
|
372
|
+
# 换行是字面换行、零转义开销。是「工具返回 JSON 字符串」约定的有意例外。
|
|
373
|
+
return base.render_tree(detail=detail, gaps=gaps)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
return _err(e)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@mcp.tool()
|
|
379
|
+
def check_consistency() -> str:
|
|
380
|
+
"""Global lint of the .gbc graph. Reports two classes, distinguished by `type`:
|
|
381
|
+
|
|
382
|
+
Errors (graph inconsistency): dangling_guarantee / missing_reverse / missing_forward.
|
|
383
|
+
Disabled notices (loud, not errors): disabled_guarantee (a guarantee with born-green
|
|
384
|
+
suspended) / depends_on_disabled (a consumer relying on a disabled guarantee).
|
|
385
|
+
|
|
386
|
+
The list is empty ONLY when fully consistent AND nothing is disabled — so any disabled
|
|
387
|
+
guarantee keeps this non-empty until it's enabled back. Filter by `type` to separate
|
|
388
|
+
hard errors from disabled notices.
|
|
389
|
+
|
|
390
|
+
Returns: JSON array of objects (empty array = consistent and nothing disabled)
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
return _ok(base.check_consistency())
|
|
394
|
+
except Exception as e:
|
|
395
|
+
return _err(e)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ======== 验证 ========
|
|
399
|
+
|
|
400
|
+
@mcp.tool()
|
|
401
|
+
def verify_provider(provider: str, auto_run_max_heavy: int = 0, timeout: int = -1) -> str:
|
|
402
|
+
"""Verify all guarantees a provider offers. Guarantees with heavy > auto_run_max_heavy
|
|
403
|
+
are skipped and reported (not failed). Gate is green iff `failed` is empty.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
provider: Source file path
|
|
407
|
+
auto_run_max_heavy: Run only guarantees with heavy <= this (default 0); higher ones are skipped
|
|
408
|
+
timeout: Global timeout override; -1 uses per-guarantee/executor default
|
|
409
|
+
|
|
410
|
+
Returns: JSON {passed, failed, skipped, results, green}
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
summary = base.verify_provider(provider, auto_run_max_heavy=auto_run_max_heavy, timeout=timeout)
|
|
414
|
+
payload = summary.model_dump()
|
|
415
|
+
payload["green"] = summary.green
|
|
416
|
+
return _ok(payload)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
return _err(e)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@mcp.tool()
|
|
422
|
+
def verify_guarantee(provider: str, id: str, timeout: int = -1) -> str:
|
|
423
|
+
"""Verify a single guarantee by id — always runs, ignoring the heavy threshold.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
provider: Source file path
|
|
427
|
+
id: Guarantee id
|
|
428
|
+
timeout: Timeout override; -1 uses guarantee/executor default
|
|
429
|
+
|
|
430
|
+
Returns: JSON verify result {return_code, stdout, stderr}
|
|
431
|
+
"""
|
|
432
|
+
try:
|
|
433
|
+
return _ok(base.verify_guarantee(provider, id, timeout=timeout).model_dump())
|
|
434
|
+
except Exception as e:
|
|
435
|
+
return _err(e)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ======== Executors ========
|
|
439
|
+
|
|
440
|
+
@mcp.tool()
|
|
441
|
+
def upsert_executor(config_name: str, config_data: dict) -> str:
|
|
442
|
+
"""Create or update an executor configuration (how to run tests).
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
config_name: Executor name (e.g. "pytest_conda")
|
|
446
|
+
config_data: {command: [parts with {file} placeholder], cwd, timeout, env_ops:[{key,action,value}]}
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
base.upsert_executor(config_name, config_data)
|
|
450
|
+
return "upserted"
|
|
451
|
+
except Exception as e:
|
|
452
|
+
return _err(e)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# ======== 意图文档(gbc.md:与 cli(gbc doc)/editor 对称的第三个薄表面)========
|
|
456
|
+
# 读(show/check)与写(set-*/sync/migrate)都经 MCP;写入的「改前须人类确认」闸门
|
|
457
|
+
# 由用户 agent 框架(hook/rules)承担——MCP 与 CLI 对称,隐藏写入通道不产生额外安全。
|
|
458
|
+
|
|
459
|
+
def _doc_root():
|
|
460
|
+
"""当前目标项目的 gbc 根(镜像层)。与 intent.cli._gbc_root 同源。"""
|
|
461
|
+
from gbc.app.config.project import get_current_project
|
|
462
|
+
gbc_root, _ = intent_base.resolve_gbc(str(get_current_project()))
|
|
463
|
+
return gbc_root
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@mcp.tool()
|
|
467
|
+
def doc_show(folder: str = "") -> str:
|
|
468
|
+
"""Show a folder's intent / internal constraints / entries from its gbc.md.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
folder: project-relative folder path; use "" for the root.
|
|
472
|
+
|
|
473
|
+
Returns: plain text rendering (same as `gbc doc show`).
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
return intent_base.show(_doc_root(), folder)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
return _err(e)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@mcp.tool()
|
|
482
|
+
def doc_check() -> str:
|
|
483
|
+
"""Whole-tree intent consistency check (DRIFT/ORPHAN are errors, STUB is a note).
|
|
484
|
+
|
|
485
|
+
Returns: JSON {errors: [...], notes: [...]} (errors empty = tree consistent).
|
|
486
|
+
"""
|
|
487
|
+
try:
|
|
488
|
+
errors, notes = intent_base.check(_doc_root())
|
|
489
|
+
return _ok({"errors": errors, "notes": notes})
|
|
490
|
+
except Exception as e:
|
|
491
|
+
return _err(e)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@mcp.tool()
|
|
495
|
+
def doc_set_intent(folder: str, text: str) -> str:
|
|
496
|
+
"""Set a folder's intent (auto single-source projection into the parent doc entry).
|
|
497
|
+
|
|
498
|
+
Writing intent changes the human-held architecture truth — your agent framework's
|
|
499
|
+
hook/rules decide whether this needs human sign-off; GBC does not gate it here.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
folder: project-relative folder path ("" for root).
|
|
503
|
+
text: the intent prose.
|
|
504
|
+
|
|
505
|
+
Returns: JSON list of written gbc.md paths.
|
|
506
|
+
"""
|
|
507
|
+
try:
|
|
508
|
+
return _ok([str(p) for p in intent_base.set_intent(_doc_root(), folder, text)])
|
|
509
|
+
except Exception as e:
|
|
510
|
+
return _err(e)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@mcp.tool()
|
|
514
|
+
def doc_set_constraints(folder: str, text: str) -> str:
|
|
515
|
+
"""Set a folder's internal constraints (local only, not projected to the parent).
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
folder: project-relative folder path ("" for root).
|
|
519
|
+
text: the constraints prose.
|
|
520
|
+
|
|
521
|
+
Returns: JSON list of written gbc.md paths.
|
|
522
|
+
"""
|
|
523
|
+
try:
|
|
524
|
+
return _ok([str(p) for p in intent_base.set_constraints(_doc_root(), folder, text)])
|
|
525
|
+
except Exception as e:
|
|
526
|
+
return _err(e)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@mcp.tool()
|
|
530
|
+
def doc_set_file(folder: str, name: str, desc: str) -> str:
|
|
531
|
+
"""Add/update a file entry in a folder's gbc.md (name must not contain '/').
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
folder: project-relative folder path ("" for root).
|
|
535
|
+
name: file name (no slash).
|
|
536
|
+
desc: the file's description.
|
|
537
|
+
|
|
538
|
+
Returns: JSON list of written gbc.md paths.
|
|
539
|
+
"""
|
|
540
|
+
try:
|
|
541
|
+
return _ok([str(p) for p in intent_base.set_file(_doc_root(), folder, name, desc)])
|
|
542
|
+
except Exception as e:
|
|
543
|
+
return _err(e)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@mcp.tool()
|
|
547
|
+
def doc_rm_entry(folder: str, name: str) -> str:
|
|
548
|
+
"""Remove an entry from a folder's gbc.md (doc only; the on-disk file is left for git review).
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
folder: project-relative folder path ("" for root).
|
|
552
|
+
name: entry name to remove.
|
|
553
|
+
|
|
554
|
+
Returns: JSON list of written gbc.md paths.
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
return _ok([str(p) for p in intent_base.rm_entry(_doc_root(), folder, name)])
|
|
558
|
+
except Exception as e:
|
|
559
|
+
return _err(e)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@mcp.tool()
|
|
563
|
+
def doc_sync() -> str:
|
|
564
|
+
"""Deterministically fix DRIFT/ORPHAN: re-project child intents into parent entries.
|
|
565
|
+
|
|
566
|
+
Returns: JSON list of fix descriptions (empty = nothing to sync).
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
return _ok(intent_base.sync(_doc_root()))
|
|
570
|
+
except Exception as e:
|
|
571
|
+
return _err(e)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@mcp.tool()
|
|
575
|
+
def doc_migrate() -> str:
|
|
576
|
+
"""Upgrade all gbc.md files to the latest format.
|
|
577
|
+
|
|
578
|
+
Returns: JSON list of migrated paths (empty = all up to date).
|
|
579
|
+
"""
|
|
580
|
+
try:
|
|
581
|
+
return _ok(intent_base.migrate(_doc_root()))
|
|
582
|
+
except Exception as e:
|
|
583
|
+
return _err(e)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ======== 入口(MCP 表面自己的启动器)========
|
|
587
|
+
|
|
588
|
+
def run_server(project_root: str | None = None) -> None:
|
|
589
|
+
"""以 stdio transport 启动 gbc MCP server。
|
|
590
|
+
|
|
591
|
+
为什么这样:GBC 常被从任意 cwd(甚至 WSL→Windows)拉起,实测环境变量传递
|
|
592
|
+
不可靠、目标项目可能有同名包按 cwd 抢占 import。因此项目根由**显式参数**
|
|
593
|
+
传入,而非依赖 env 或 cwd。stdout 是 JSON-RPC 协议通道,强制 UTF-8避免本地码页污染。
|
|
594
|
+
"""
|
|
595
|
+
import sys
|
|
596
|
+
from pathlib import Path
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
600
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
601
|
+
except Exception:
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
if project_root:
|
|
605
|
+
from gbc.app.config import project
|
|
606
|
+
project.set_current_project(str(Path(project_root).expanduser()))
|
|
607
|
+
|
|
608
|
+
mcp.run()
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def main() -> None:
|
|
612
|
+
mcp.run()
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
if __name__ == "__main__":
|
|
616
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
|