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.
Files changed (48) hide show
  1. gbc/__init__.py +21 -0
  2. gbc/app/__init__.py +14 -0
  3. gbc/app/assets.py +38 -0
  4. gbc/app/config/__init__.py +14 -0
  5. gbc/app/config/backups.py +28 -0
  6. gbc/app/config/base.py +29 -0
  7. gbc/app/config/executor.py +132 -0
  8. gbc/app/config/project.py +35 -0
  9. gbc/app/core/__init__.py +14 -0
  10. gbc/app/core/env.py +64 -0
  11. gbc/app/core/executor.py +116 -0
  12. gbc/app/core/guarantee.py +338 -0
  13. gbc/app/i18n/__init__.py +43 -0
  14. gbc/app/i18n/lang.py +88 -0
  15. gbc/app/i18n/translate.py +80 -0
  16. gbc/app/intent/__init__.py +20 -0
  17. gbc/app/intent/base.py +308 -0
  18. gbc/app/intent/cli.py +124 -0
  19. gbc/app/intent/editor.py +93 -0
  20. gbc/app/interface/__init__.py +14 -0
  21. gbc/app/interface/base.py +851 -0
  22. gbc/app/interface/cli.py +585 -0
  23. gbc/app/interface/mcp.py +616 -0
  24. gbc/app/models/__init__.py +14 -0
  25. gbc/app/models/errors.py +179 -0
  26. gbc/app/models/meta.py +92 -0
  27. gbc/app/models/verify.py +63 -0
  28. gbc/app/utils/__init__.py +14 -0
  29. gbc/app/utils/file_utils.py +24 -0
  30. gbc/app/utils/gbc_md.py +121 -0
  31. gbc/app/utils/json_model_operator.py +85 -0
  32. gbc/app/utils/safe_file_writer.py +158 -0
  33. gbc/assets/editor/index.html +299 -0
  34. gbc/assets/i18n/catalog/en.json +52 -0
  35. gbc/assets/i18n/catalog/zh.json +52 -0
  36. gbc/assets/i18n/texts/rules.en.md +30 -0
  37. gbc/assets/i18n/texts/rules.zh.md +24 -0
  38. gbc/assets/i18n/texts/setup.en.md +74 -0
  39. gbc/assets/i18n/texts/setup.zh.md +69 -0
  40. gbc/assets/skills/README.md +16 -0
  41. gbc/assets/skills/gbc-cli/SKILL.md +143 -0
  42. gbc/entry.py +126 -0
  43. guarantee_based_coding-0.2.0.dist-info/METADATA +108 -0
  44. guarantee_based_coding-0.2.0.dist-info/RECORD +48 -0
  45. guarantee_based_coding-0.2.0.dist-info/WHEEL +5 -0
  46. guarantee_based_coding-0.2.0.dist-info/entry_points.txt +2 -0
  47. guarantee_based_coding-0.2.0.dist-info/licenses/LICENSE +202 -0
  48. guarantee_based_coding-0.2.0.dist-info/top_level.txt +1 -0
@@ -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
+