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
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
# REQUIREMENTS
|
|
16
|
+
# None, raw python should have all libs required
|
|
17
|
+
# From https://github.com/Jesse-x86/devkit/blob/master/file_operations/safe_file_writer.py
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SafeFileWriter:
|
|
25
|
+
"""
|
|
26
|
+
一个安全的文件写入器,通过上下文管理器(with语句)提供原子写入和自动备份功能。
|
|
27
|
+
|
|
28
|
+
功能特性:
|
|
29
|
+
1. **原子性**: 先写入到一个临时文件,只有在with代码块成功执行完毕后,
|
|
30
|
+
才会用新内容覆盖原文件,有效防止因写入中断导致的文件损坏。
|
|
31
|
+
2. **自动备份**: 在覆盖原文件之前,会自动将旧文件备份。
|
|
32
|
+
3. **备份管理**: 自动维护指定数量的备份文件(如 .bak1, .bak2 ...),
|
|
33
|
+
并删除更早的备份。
|
|
34
|
+
4. **自动回滚**: 如果with代码块中发生任何异常,临时文件将被删除,
|
|
35
|
+
原始文件和备份文件将保持不变,实现自动回滚。
|
|
36
|
+
5. **灵活写入**: 调用方在with块内可以获得一个标准的文件句柄,可以像操作
|
|
37
|
+
普通文件一样进行写入(write, writelines, print(file=f)等),
|
|
38
|
+
完全控制写入的内容和方式。
|
|
39
|
+
|
|
40
|
+
使用示例:
|
|
41
|
+
writer = SafeFileWriter('my_data.json', num_backups=3)
|
|
42
|
+
try:
|
|
43
|
+
with writer.open(mode='w', encoding='utf-8') as f:
|
|
44
|
+
# f 是一个真正的文件句柄,可以自由写入
|
|
45
|
+
import json
|
|
46
|
+
json.dump({'key': 'new value'}, f, indent=4)
|
|
47
|
+
# 假设这里发生错误
|
|
48
|
+
# raise ValueError("写入过程中发生错误")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"写入失败: {e}")
|
|
51
|
+
|
|
52
|
+
# 如果成功,'my_data.json' 会被更新,旧文件变为 'my_data.json.bak1'
|
|
53
|
+
# 如果失败,'my_data.json' 保持原样,不会产生不完整的文件。
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, filepath: str | Path, num_backups: int = 0):
|
|
57
|
+
"""
|
|
58
|
+
初始化SafeFileWriter。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
filepath (str | Path): 目标文件的完整路径。
|
|
62
|
+
num_backups (int, optional): 需要保留的最大备份数量。默认为 0。
|
|
63
|
+
如果为0,则不创建备份,但仍保证原子写入。
|
|
64
|
+
"""
|
|
65
|
+
self._mode = None
|
|
66
|
+
self._open_kwargs = None
|
|
67
|
+
self.filepath = Path(filepath)
|
|
68
|
+
self.num_backups = max(0, num_backups)
|
|
69
|
+
self._temp_path = None
|
|
70
|
+
self._file_handle = None
|
|
71
|
+
self._logger = logging.getLogger("SafeFileWriter")
|
|
72
|
+
|
|
73
|
+
def _log(self, msg: str):
|
|
74
|
+
self._logger.debug(msg)
|
|
75
|
+
|
|
76
|
+
def _rotate_backups(self):
|
|
77
|
+
"""管理和轮转备份文件。"""
|
|
78
|
+
if not self.filepath.exists() or self.num_backups == 0:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# 1. 删除最旧的备份 (如果存在)
|
|
82
|
+
oldest_bak = self.filepath.with_suffix(f"{self.filepath.suffix}.bak{self.num_backups}")
|
|
83
|
+
if oldest_bak.exists():
|
|
84
|
+
oldest_bak.unlink()
|
|
85
|
+
|
|
86
|
+
# 2. 将现有备份序号+1
|
|
87
|
+
# 从后往前重命名,避免覆盖
|
|
88
|
+
for i in range(self.num_backups - 1, 0, -1):
|
|
89
|
+
src_bak = self.filepath.with_suffix(f"{self.filepath.suffix}.bak{i}")
|
|
90
|
+
dst_bak = self.filepath.with_suffix(f"{self.filepath.suffix}.bak{i + 1}")
|
|
91
|
+
if src_bak.exists():
|
|
92
|
+
shutil.move(str(src_bak), str(dst_bak))
|
|
93
|
+
|
|
94
|
+
# 3. 将当前文件创建为第一个备份
|
|
95
|
+
first_bak = self.filepath.with_suffix(f"{self.filepath.suffix}.bak1")
|
|
96
|
+
shutil.move(str(self.filepath), str(first_bak))
|
|
97
|
+
self._log(f"备份: {self.filepath.name} -> {first_bak.name}")
|
|
98
|
+
|
|
99
|
+
def open(self, mode='w', **kwargs):
|
|
100
|
+
"""
|
|
101
|
+
以上下文管理器的方式打开文件准备写入。
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
mode (str, optional): 文件打开模式,推荐使用 'w' (文本) 或 'wb' (二进制)。
|
|
105
|
+
追加模式 'a' 在此逻辑下意义不大。默认为 'w'。
|
|
106
|
+
**kwargs: 传递给内建 open() 函数的其他参数, 如 encoding, errors等。
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
一个上下文管理器对象。
|
|
110
|
+
"""
|
|
111
|
+
self._mode = mode
|
|
112
|
+
self._open_kwargs = kwargs
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __enter__(self):
|
|
116
|
+
"""上下文管理器的进入方法,准备临时文件。"""
|
|
117
|
+
# 创建一个唯一的临时文件
|
|
118
|
+
self._temp_path = self.filepath.with_suffix(f"{self.filepath.suffix}.{os.urandom(6).hex()}.tmp")
|
|
119
|
+
|
|
120
|
+
# 打开临时文件用于写入,并将文件句柄返回给 `with ... as f:`
|
|
121
|
+
self._file_handle = open(self._temp_path, self._mode, **self._open_kwargs)
|
|
122
|
+
return self._file_handle
|
|
123
|
+
|
|
124
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
125
|
+
"""
|
|
126
|
+
上下文管理器的退出方法,处理提交或回滚。
|
|
127
|
+
|
|
128
|
+
exc_type, exc_val, exc_tb: 如果with块内有异常,这些是异常信息,否则为None。
|
|
129
|
+
"""
|
|
130
|
+
# 1. 必须确保文件句柄被关闭
|
|
131
|
+
if self._file_handle and not self._file_handle.closed:
|
|
132
|
+
self._file_handle.close()
|
|
133
|
+
|
|
134
|
+
# 2. 检查with块是否成功执行
|
|
135
|
+
if exc_type is None:
|
|
136
|
+
# 成功: 执行备份和替换
|
|
137
|
+
try:
|
|
138
|
+
self._log(f"写入成功,准备更新文件: {self.filepath.name}")
|
|
139
|
+
# a. 轮转备份
|
|
140
|
+
if self.num_backups > 0:
|
|
141
|
+
self._rotate_backups()
|
|
142
|
+
# b. 将临时文件重命名为目标文件 (原子操作)
|
|
143
|
+
shutil.move(str(self._temp_path), str(self.filepath))
|
|
144
|
+
self._log(f"文件已更新: {self.filepath.name}")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
self._log(f"错误:在提交文件时发生意外: {e}")
|
|
147
|
+
# 如果提交阶段也失败了,尝试清理临时文件
|
|
148
|
+
if self._temp_path.exists():
|
|
149
|
+
self._temp_path.unlink()
|
|
150
|
+
# 让外部知道提交失败了
|
|
151
|
+
raise
|
|
152
|
+
else:
|
|
153
|
+
# 失败: 清理临时文件,不触碰原文件和备份
|
|
154
|
+
self._log(f"写入失败,正在回滚...")
|
|
155
|
+
if self._temp_path.exists():
|
|
156
|
+
self._temp_path.unlink()
|
|
157
|
+
self._log(f"已删除临时文件: {self._temp_path.name}")
|
|
158
|
+
# 异常会由Python自动重新抛出,调用方可以捕获它
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>GBC 意图树编辑器</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --bd:#d0d0d8; --mut:#777; --accent:#3b6ea5; --dir:#2e7d32; }
|
|
9
|
+
* { box-sizing: border-box; }
|
|
10
|
+
body { margin:0; font:14px/1.5 system-ui, "Segoe UI", sans-serif; color:#222; }
|
|
11
|
+
header { display:flex; gap:8px; align-items:center; padding:8px 12px;
|
|
12
|
+
border-bottom:1px solid var(--bd); background:#fafafc; }
|
|
13
|
+
header input { flex:1; padding:5px 8px; font:inherit; border:1px solid var(--bd); border-radius:4px; }
|
|
14
|
+
button { font:inherit; padding:5px 10px; border:1px solid var(--bd); background:#fff;
|
|
15
|
+
border-radius:4px; cursor:pointer; }
|
|
16
|
+
button:hover { background:#f0f0f4; }
|
|
17
|
+
button.primary { background:var(--accent); color:#fff; border-color:var(--accent); }
|
|
18
|
+
button:disabled { opacity:.5; cursor:default; }
|
|
19
|
+
.layout { display:flex; height:calc(100vh - 49px); }
|
|
20
|
+
.tree { width:300px; overflow:auto; border-right:1px solid var(--bd); padding:8px; }
|
|
21
|
+
.editor { flex:1; overflow:auto; padding:16px 22px; }
|
|
22
|
+
.trow { display:flex; align-items:center; white-space:nowrap; }
|
|
23
|
+
.tog { width:16px; text-align:center; cursor:pointer; color:var(--mut); user-select:none; }
|
|
24
|
+
.tog.leaf { cursor:default; color:#ccc; }
|
|
25
|
+
.tlabel { cursor:pointer; padding:2px 6px; border-radius:4px; flex:1; }
|
|
26
|
+
.tlabel:hover { background:#eef; }
|
|
27
|
+
.tlabel.sel { background:#dde7f3; font-weight:600; }
|
|
28
|
+
.twrap { margin-left:14px; border-left:1px dotted #ccc; padding-left:6px; }
|
|
29
|
+
label { display:block; font-weight:600; margin:14px 0 4px; }
|
|
30
|
+
textarea { width:100%; font:inherit; padding:6px 8px; border:1px solid var(--bd);
|
|
31
|
+
border-radius:4px; resize:none; overflow:hidden; min-height:2.4em; }
|
|
32
|
+
.entry { border:1px solid var(--bd); border-radius:6px; padding:10px; margin:8px 0; background:#fcfcfe; }
|
|
33
|
+
.entry.ghost { opacity:.55; border-style:dashed; background:#f5f5f7; }
|
|
34
|
+
.entry .row { display:flex; gap:8px; align-items:center; }
|
|
35
|
+
.entry input.name { font:inherit; padding:4px 6px; border:1px solid var(--bd); border-radius:4px; }
|
|
36
|
+
.badge { font-size:12px; color:#fff; background:var(--dir); padding:1px 6px; border-radius:10px; }
|
|
37
|
+
.badge.file { background:#888; }
|
|
38
|
+
.mut { color:var(--mut); font-size:12px; }
|
|
39
|
+
.crumb { color:var(--mut); font-size:13px; margin-bottom:4px; }
|
|
40
|
+
.mirror-note { font-size:12px; color:var(--dir); margin-top:3px; }
|
|
41
|
+
.msg { margin-left:auto; color:var(--mut); }
|
|
42
|
+
.empty { padding:24px; color:#777; }
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div id="root"></div>
|
|
47
|
+
<script>
|
|
48
|
+
// ---- tiny DOM helper -------------------------------------------------------
|
|
49
|
+
function el(tag, props, ...kids) {
|
|
50
|
+
const n = document.createElement(tag);
|
|
51
|
+
for (const k in (props || {})) {
|
|
52
|
+
if (k === "class") n.className = props[k];
|
|
53
|
+
else if (k.startsWith("on")) n.addEventListener(k.slice(2), props[k]);
|
|
54
|
+
else if (k === "value") n.value = props[k];
|
|
55
|
+
else if (k === "disabled") { if (props[k]) n.disabled = true; }
|
|
56
|
+
else n.setAttribute(k, props[k]);
|
|
57
|
+
}
|
|
58
|
+
for (const c of kids.flat()) {
|
|
59
|
+
if (c == null || c === false) continue;
|
|
60
|
+
n.append(c.nodeType ? c : document.createTextNode(String(c)));
|
|
61
|
+
}
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
function sizeTa(t) { t.style.height = "auto"; t.style.height = (t.scrollHeight + 2) + "px"; }
|
|
65
|
+
function ta(props) {
|
|
66
|
+
return el("textarea", { ...props,
|
|
67
|
+
oninput: (ev) => { if (props.oninput) props.oninput(ev); sizeTa(ev.target); } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- state -----------------------------------------------------------------
|
|
71
|
+
const state = { root: "", tree: null, sel: null, msg: "" };
|
|
72
|
+
const collapsed = new Set();
|
|
73
|
+
let _uid = 1;
|
|
74
|
+
|
|
75
|
+
const HIST_KEY = "gbc_dir_history";
|
|
76
|
+
const getHistory = () => { try { return JSON.parse(localStorage.getItem(HIST_KEY)) || []; } catch { return []; } };
|
|
77
|
+
function pushHistory(p) {
|
|
78
|
+
if (!p) return;
|
|
79
|
+
let h = getHistory().filter(x => x !== p);
|
|
80
|
+
h.unshift(p);
|
|
81
|
+
localStorage.setItem(HIST_KEY, JSON.stringify(h.slice(0, 12)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// entry kind derived purely from the name's trailing "/"
|
|
85
|
+
const isDir = (e) => (e.name || "").trim().endsWith("/");
|
|
86
|
+
const folderName = (e) => (e.name || "").trim().replace(/\/+$/, "");
|
|
87
|
+
const bodyText = (e) => isDir(e) ? (e.child ? (e.child.intent || "") : "") : (e.desc || "");
|
|
88
|
+
function childHasContent(e) {
|
|
89
|
+
const c = e.child;
|
|
90
|
+
if (!c) return false;
|
|
91
|
+
if ((c.intent || "").trim() || (c.constraints || "").trim()) return true;
|
|
92
|
+
return (c.entries || []).some(x => (x.name || "").trim() || bodyText(x).trim() || childHasContent(x));
|
|
93
|
+
}
|
|
94
|
+
// an entry is a "ghost" (grey / deletable) when it carries no content at all
|
|
95
|
+
const isEmpty = (e) => !(e.name || "").trim() && !bodyText(e).trim() && !childHasContent(e);
|
|
96
|
+
|
|
97
|
+
function assignIds(node) {
|
|
98
|
+
node._id = _uid++;
|
|
99
|
+
for (const e of node.entries) if (e.child) assignIds(e.child);
|
|
100
|
+
}
|
|
101
|
+
function findById(node, id, path) {
|
|
102
|
+
if (node._id === id) return { node, path };
|
|
103
|
+
for (const e of node.entries) {
|
|
104
|
+
if (isDir(e) && e.child) {
|
|
105
|
+
const r = findById(e.child, id, path ? path + "/" + folderName(e) : folderName(e));
|
|
106
|
+
if (r) return r;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function ensureChild(e) {
|
|
112
|
+
if (!e.child) e.child = { name: folderName(e), intent: "", constraints: "", entries: [], _id: _uid++ };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- data ops --------------------------------------------------------------
|
|
116
|
+
async function load() {
|
|
117
|
+
setMsg("加载中…");
|
|
118
|
+
try {
|
|
119
|
+
const r = await fetch("/api/tree?root=" + encodeURIComponent(state.root));
|
|
120
|
+
const t = await r.json();
|
|
121
|
+
if (!r.ok) throw new Error(t.error || r.statusText);
|
|
122
|
+
assignIds(t);
|
|
123
|
+
state.tree = t; state.sel = t._id; collapsed.clear();
|
|
124
|
+
pushHistory(state.root); setMsg("已加载"); render();
|
|
125
|
+
} catch (err) { setMsg("加载失败:" + err.message); }
|
|
126
|
+
}
|
|
127
|
+
async function save() {
|
|
128
|
+
setMsg("保存中…");
|
|
129
|
+
try {
|
|
130
|
+
const r = await fetch("/api/tree", {
|
|
131
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ root: state.root, tree: state.tree }),
|
|
133
|
+
});
|
|
134
|
+
const res = await r.json();
|
|
135
|
+
if (!r.ok) throw new Error(res.error || r.statusText);
|
|
136
|
+
pushHistory(state.root); setMsg("已写入 " + res.count + " 个 gbc.md"); render();
|
|
137
|
+
} catch (err) { setMsg("保存失败:" + err.message); }
|
|
138
|
+
}
|
|
139
|
+
function setMsg(m) { state.msg = m; const s = document.querySelector(".msg"); if (s) s.textContent = m; }
|
|
140
|
+
|
|
141
|
+
// ---- sidebar tree ----------------------------------------------------------
|
|
142
|
+
function treeView(node) {
|
|
143
|
+
const dirKids = node.entries.filter(e => isDir(e) && e.child);
|
|
144
|
+
const hasKids = dirKids.length > 0;
|
|
145
|
+
const open = !collapsed.has(node._id);
|
|
146
|
+
const toggle = el("span",
|
|
147
|
+
{ class: "tog" + (hasKids ? "" : " leaf"),
|
|
148
|
+
onclick: () => { if (!hasKids) return; open ? collapsed.add(node._id) : collapsed.delete(node._id); render(); } },
|
|
149
|
+
hasKids ? (open ? "▾" : "▸") : "•");
|
|
150
|
+
const label = el("span",
|
|
151
|
+
{ class: "tlabel" + (node._id === state.sel ? " sel" : ""),
|
|
152
|
+
onclick: () => { state.sel = node._id; render(); } },
|
|
153
|
+
node.name || "(root)");
|
|
154
|
+
const kids = (hasKids && open) ? el("div", { class: "twrap" }, dirKids.map(e => treeView(e.child))) : null;
|
|
155
|
+
return el("div", {}, el("div", { class: "trow" }, toggle, label), kids);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- one entry row: self-managing, keeps focus on type-flip ----------------
|
|
159
|
+
function entryRow(node, e, onGrow) {
|
|
160
|
+
const root = el("div", { class: "entry" });
|
|
161
|
+
const bodyWrap = el("div", {});
|
|
162
|
+
// All row elements are persistent — we NEVER detach nameInput, so typing "/"
|
|
163
|
+
// (which flips the kind) keeps focus and caret exactly where they are.
|
|
164
|
+
const badge = el("span", {});
|
|
165
|
+
const nameInput = el("input", { class: "name", value: e.name || "",
|
|
166
|
+
placeholder: "名称(结尾带 / = 文件夹)" });
|
|
167
|
+
const enterBtn = el("button",
|
|
168
|
+
{ onclick: () => { ensureChild(e); state.sel = e.child._id; render(); } }, "进入 →");
|
|
169
|
+
const delBtn = el("button",
|
|
170
|
+
{ onclick: () => { const i = node.entries.indexOf(e); if (i >= 0) node.entries.splice(i, 1); render(); } },
|
|
171
|
+
"删除");
|
|
172
|
+
const rowDiv = el("div", { class: "row" }, badge, nameInput, enterBtn, delBtn);
|
|
173
|
+
|
|
174
|
+
const paintGrey = () => root.classList.toggle("ghost", isEmpty(e));
|
|
175
|
+
|
|
176
|
+
function paintKind() {
|
|
177
|
+
const dir = isDir(e);
|
|
178
|
+
badge.className = "badge" + (dir ? "" : " file");
|
|
179
|
+
badge.textContent = dir ? "文件夹" : "文件";
|
|
180
|
+
enterBtn.style.display = dir ? "" : "none";
|
|
181
|
+
|
|
182
|
+
bodyWrap.innerHTML = ""; // bodyWrap holds no focusable name field — safe
|
|
183
|
+
if (dir) {
|
|
184
|
+
ensureChild(e);
|
|
185
|
+
// carry typed text across a file -> folder flip so nothing is lost
|
|
186
|
+
if (!(e.child.intent || "").trim() && (e.desc || "").trim()) { e.child.intent = e.desc; e.desc = ""; }
|
|
187
|
+
bodyWrap.append(ta({ rows: 1, value: e.child.intent || "",
|
|
188
|
+
oninput: ev => { e.child.intent = ev.target.value; paintGrey(); onGrow(); } }));
|
|
189
|
+
bodyWrap.append(el("div", { class: "mirror-note" },
|
|
190
|
+
"↔ 同一字段:这就是 " + (folderName(e) || "该文件夹") + "/gbc.md 的意图,改这里=改那里"));
|
|
191
|
+
} else {
|
|
192
|
+
if (e.desc == null) e.desc = "";
|
|
193
|
+
// carry text back on a folder -> file flip
|
|
194
|
+
if (!e.desc.trim() && e.child && (e.child.intent || "").trim()) { e.desc = e.child.intent; }
|
|
195
|
+
bodyWrap.append(ta({ rows: 1, value: e.desc,
|
|
196
|
+
oninput: ev => { e.desc = ev.target.value; paintGrey(); onGrow(); } }));
|
|
197
|
+
}
|
|
198
|
+
requestAnimationFrame(() => bodyWrap.querySelectorAll("textarea").forEach(sizeTa));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
nameInput.addEventListener("input", ev => {
|
|
202
|
+
const was = isDir(e);
|
|
203
|
+
e.name = ev.target.value;
|
|
204
|
+
if (isDir(e) !== was) paintKind(); // in-place: nameInput is untouched, focus stays
|
|
205
|
+
paintGrey(); onGrow();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
root.append(rowDiv, bodyWrap);
|
|
209
|
+
paintKind(); paintGrey();
|
|
210
|
+
return root;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- entries section: trailing ghost + prune-on-blur -----------------------
|
|
214
|
+
function entriesSection(node) {
|
|
215
|
+
const container = el("div", {});
|
|
216
|
+
const ensureGhost = () => {
|
|
217
|
+
const last = node.entries[node.entries.length - 1];
|
|
218
|
+
if (!last || !isEmpty(last)) {
|
|
219
|
+
const g = { name: "", desc: "" };
|
|
220
|
+
node.entries.push(g);
|
|
221
|
+
container.append(entryRow(node, g, ensureGhost));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const rebuild = () => {
|
|
225
|
+
container.innerHTML = "";
|
|
226
|
+
node.entries.forEach(e => container.append(entryRow(node, e, ensureGhost)));
|
|
227
|
+
ensureGhost();
|
|
228
|
+
};
|
|
229
|
+
rebuild();
|
|
230
|
+
container.addEventListener("focusout", () => setTimeout(() => {
|
|
231
|
+
if (container.contains(document.activeElement)) return; // still editing in here
|
|
232
|
+
const kept = node.entries.filter(e => !isEmpty(e)); // drop emptied rows
|
|
233
|
+
node.entries.length = 0; kept.forEach(e => node.entries.push(e));
|
|
234
|
+
rebuild();
|
|
235
|
+
}, 0));
|
|
236
|
+
return container;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- editor panel ----------------------------------------------------------
|
|
240
|
+
function editor(node, path) {
|
|
241
|
+
const segs = (path || "").split("/").filter(Boolean);
|
|
242
|
+
const myName = segs.length ? segs[segs.length - 1] : node.name;
|
|
243
|
+
return el("div", {},
|
|
244
|
+
el("div", { class: "crumb" }, path || "(根)"),
|
|
245
|
+
path ? el("div", { class: "mut" },
|
|
246
|
+
"↔ 此意图同时显示在父节点的 ## " + myName + "/ 条目里(单一源,双处投影)") : null,
|
|
247
|
+
|
|
248
|
+
el("label", {}, "意图 ", el("span", { class: "mut" }, "(# 意图)")),
|
|
249
|
+
ta({ rows: 1, value: node.intent || "", oninput: ev => { node.intent = ev.target.value; } }),
|
|
250
|
+
|
|
251
|
+
el("label", {}, "内部约束 ",
|
|
252
|
+
el("span", { class: "mut" }, "(# 内部约束 · 可选 · 不冒泡到父节点)")),
|
|
253
|
+
ta({ rows: 1, value: node.constraints || "", oninput: ev => { node.constraints = ev.target.value; } }),
|
|
254
|
+
|
|
255
|
+
el("label", {}, "子项 ",
|
|
256
|
+
el("span", { class: "mut" }, "(# 文件 段下的 ## · 名称结尾带 / 即文件夹;底部灰条输入即新增,清空失焦即删除)")),
|
|
257
|
+
entriesSection(node));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---- top-level render ------------------------------------------------------
|
|
261
|
+
function render() {
|
|
262
|
+
const rootEl = document.getElementById("root");
|
|
263
|
+
rootEl.innerHTML = "";
|
|
264
|
+
|
|
265
|
+
const hist = getHistory();
|
|
266
|
+
rootEl.append(el("header", {},
|
|
267
|
+
el("input", { value: state.root, placeholder: ".gbc 目录的绝对路径", list: "histlist",
|
|
268
|
+
oninput: ev => { state.root = ev.target.value; } }),
|
|
269
|
+
el("datalist", { id: "histlist" }, hist.map(p => el("option", { value: p }))),
|
|
270
|
+
el("button", { onclick: load }, "加载"),
|
|
271
|
+
el("button", { class: "primary", disabled: !state.tree, onclick: save }, "保存"),
|
|
272
|
+
el("span", { class: "msg" }, state.msg)));
|
|
273
|
+
|
|
274
|
+
if (!state.tree) {
|
|
275
|
+
rootEl.append(el("div", { class: "empty" },
|
|
276
|
+
"输入一个 .gbc 目录路径点「加载」。路径不存在也行 —— 会给你一棵空树,保存时再创建(从零新建)。"));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const found = findById(state.tree, state.sel, "");
|
|
280
|
+
rootEl.append(el("div", { class: "layout" },
|
|
281
|
+
el("div", { class: "tree" }, treeView(state.tree)),
|
|
282
|
+
el("div", { class: "editor" }, found ? editor(found.node, found.path) : null)));
|
|
283
|
+
|
|
284
|
+
requestAnimationFrame(() => document.querySelectorAll(".editor textarea").forEach(sizeTa));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---- init: prefer --root from backend, else most-recent history ------------
|
|
288
|
+
(async function init() {
|
|
289
|
+
try {
|
|
290
|
+
const c = await (await fetch("/api/config")).json();
|
|
291
|
+
if (c.root) { state.root = c.root; render(); await load(); return; }
|
|
292
|
+
} catch { /* ignore */ }
|
|
293
|
+
const h = getHistory();
|
|
294
|
+
if (h.length) state.root = h[0];
|
|
295
|
+
render();
|
|
296
|
+
})();
|
|
297
|
+
</script>
|
|
298
|
+
</body>
|
|
299
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.language_name": "English",
|
|
3
|
+
|
|
4
|
+
"err.generic": "Error: {msg}",
|
|
5
|
+
"err.unexpected": "Unexpected error: {kind}: {msg}",
|
|
6
|
+
"err.illegal_operation": "Illegal operation: {msg}",
|
|
7
|
+
"err.config": "Config error: {msg}",
|
|
8
|
+
"err.executor": "Executor error: {msg}",
|
|
9
|
+
"err.guarantee": "Guarantee error: {msg}",
|
|
10
|
+
"err.test_failed": "Test failed: {guarantee} on {target}",
|
|
11
|
+
"err.retire_blocked": "Retire blocked: {msg}",
|
|
12
|
+
"err.executor_not_found": "Executor config '{name}' not found. Run `gbc executor upsert` to configure it.",
|
|
13
|
+
|
|
14
|
+
"exc.illegal_file_path": "Operation intended for '{target}' is illegal",
|
|
15
|
+
"exc.config_not_found": "Config file '{target}' not found",
|
|
16
|
+
"exc.config_parse": "Config file '{target}' failed to parse:\n{info}",
|
|
17
|
+
"exc.project_not_found": "Project '{target}' not found",
|
|
18
|
+
"exc.meta_not_found": "Meta file '{target}' for '{original}' not found",
|
|
19
|
+
"exc.guarantee_duplicated": "Guarantee '{gid}' already exists for '{target}'. To update it, use 'update' instead of 'create'",
|
|
20
|
+
"exc.guarantee_not_found": "Guarantee '{gid}' not found for '{target}'",
|
|
21
|
+
"exc.guarantee_test_failed": "Guarantee '{gid}' failed for '{target}', failure info:\n{info}",
|
|
22
|
+
"exc.guarantee_has_dependents": "Guarantee '{gid}' on '{provider}' still has {count} dependent(s): {dependents}. Repair or migrate them first; a guarantee can only be retired once its dependents are empty.",
|
|
23
|
+
"exc.executor_not_found": "Executor config '{name}' not found",
|
|
24
|
+
"exc.executor_config_invalid": "Parameters received for executor config '{name}' are invalid",
|
|
25
|
+
"exc.doc_filename_slash": "A file entry name must not end with '/' (for a subfolder use set-intent <folder>/<sub>): {name}",
|
|
26
|
+
"exc.doc_name_is_folder": "'{name}' is already a subfolder entry and cannot be changed into a file",
|
|
27
|
+
"exc.doc_entry_not_found": "Entry not found: {name}",
|
|
28
|
+
|
|
29
|
+
"doctor.missing_executor": "This project requires executor '{name}' but it is not configured locally. Run `gbc executor upsert` to add it.",
|
|
30
|
+
"doctor.executors_ok": "All executors required by this project are configured.",
|
|
31
|
+
|
|
32
|
+
"mcp.starting": "Starting GBC MCP server for project: {path}",
|
|
33
|
+
"editor.starting": "GBC intent editor running at http://{host}:{port}",
|
|
34
|
+
|
|
35
|
+
"cli.app.help": "GBC — Guarantee-Based Coding CLI tool",
|
|
36
|
+
"cli.guarantee.help": "Guarantee CRUD",
|
|
37
|
+
"cli.dep.help": "Dependency edge registration and reverse lookup",
|
|
38
|
+
"cli.verify.help": "Run verification",
|
|
39
|
+
"cli.doctor.help": "Consistency check",
|
|
40
|
+
"cli.executor.help": "Manage executor configurations",
|
|
41
|
+
"cli.refactor.help": "Relocate: move files/directories and fix all graph references",
|
|
42
|
+
"cli.mcp.help": "MCP server (persistent channel for agents)",
|
|
43
|
+
"cli.editor.help": "Intent editor (persistent web service for humans)",
|
|
44
|
+
"cli.option.lang.help": "UI/message language zh/en (also via GBC_LANG env var)",
|
|
45
|
+
"cli.rules.help": "Print the recommended guardrail ruleset to stdout (advisory, not a sandbox)",
|
|
46
|
+
"cli.setup.help": "Print a localized wiring guide to stdout: how to connect MCP / skills to your agent",
|
|
47
|
+
"cli.tree.help": "Render the entire .gbc tree as one AI-readable dependency document",
|
|
48
|
+
"cli.tree.option.detail": "Expand guarantee desc/test/heavy + other artifacts (.pyi) in each .gbc directory",
|
|
49
|
+
"cli.tree.option.gaps": "Append registration gaps derived from the graph (has json/is depended on but unregistered)",
|
|
50
|
+
"cli.mcp_up.arg.project_root": "Absolute path to target project root; omit to auto-detect",
|
|
51
|
+
"cli.doc.help": "Compliant read/write entry for intent docs (gbc.md)"
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta.language_name": "简体中文",
|
|
3
|
+
|
|
4
|
+
"err.generic": "错误:{msg}",
|
|
5
|
+
"err.unexpected": "意外错误:{kind}:{msg}",
|
|
6
|
+
"err.illegal_operation": "非法操作:{msg}",
|
|
7
|
+
"err.config": "配置错误:{msg}",
|
|
8
|
+
"err.executor": "执行器错误:{msg}",
|
|
9
|
+
"err.guarantee": "保证错误:{msg}",
|
|
10
|
+
"err.test_failed": "测试失败:{guarantee}(目标文件 {target})",
|
|
11
|
+
"err.retire_blocked": "退休被拦截:{msg}",
|
|
12
|
+
"err.executor_not_found": "未找到 executor 配置「{name}」。请运行 `gbc executor upsert` 进行配置。",
|
|
13
|
+
|
|
14
|
+
"exc.illegal_file_path": "针对「{target}」的操作在意图层面就是非法的",
|
|
15
|
+
"exc.config_not_found": "配置文件「{target}」未找到",
|
|
16
|
+
"exc.config_parse": "配置文件「{target}」解析失败:\n{info}",
|
|
17
|
+
"exc.project_not_found": "项目「{target}」未找到",
|
|
18
|
+
"exc.meta_not_found": "未找到「{original}」对应的 meta 文件「{target}」",
|
|
19
|
+
"exc.guarantee_duplicated": "保证「{gid}」已存在于「{target}」。若想更新保证信息,请用 update 而非 create",
|
|
20
|
+
"exc.guarantee_not_found": "「{target}」上未找到保证「{gid}」",
|
|
21
|
+
"exc.guarantee_test_failed": "保证「{gid}」在「{target}」上验证失败,失败信息:\n{info}",
|
|
22
|
+
"exc.guarantee_has_dependents": "「{provider}」上的保证「{gid}」仍有 {count} 个依赖方:{dependents}。请先修复或迁移它们;保证只有在依赖方清空后才能退休。",
|
|
23
|
+
"exc.executor_not_found": "未找到 executor 配置「{name}」",
|
|
24
|
+
"exc.executor_config_invalid": "executor 配置「{name}」收到的参数无效",
|
|
25
|
+
"exc.doc_filename_slash": "文件条目名不应以 / 结尾(子文件夹请用 set-intent <folder>/<sub>):{name}",
|
|
26
|
+
"exc.doc_name_is_folder": "「{name}」已是子文件夹条目,不能当文件改",
|
|
27
|
+
"exc.doc_entry_not_found": "未找到条目:{name}",
|
|
28
|
+
|
|
29
|
+
"doctor.missing_executor": "本项目需要 executor「{name}」,但你尚未在本地配置。请运行 `gbc executor upsert` 添加。",
|
|
30
|
+
"doctor.executors_ok": "本项目所需的 executor 均已配置。",
|
|
31
|
+
|
|
32
|
+
"mcp.starting": "正在为项目启动 GBC MCP 服务:{path}",
|
|
33
|
+
"editor.starting": "GBC 意图编辑器已启动:http://{host}:{port}",
|
|
34
|
+
|
|
35
|
+
"cli.app.help": "GBC - Guarantee-Based Coding 命令行工具",
|
|
36
|
+
"cli.guarantee.help": "保证(Guarantee)增删改查",
|
|
37
|
+
"cli.dep.help": "依赖边(Dependency)登记与反查",
|
|
38
|
+
"cli.verify.help": "运行验证",
|
|
39
|
+
"cli.doctor.help": "一致性体检",
|
|
40
|
+
"cli.executor.help": "管理执行器配置",
|
|
41
|
+
"cli.refactor.help": "重定位:移动文件/目录并修全图路径引用",
|
|
42
|
+
"cli.mcp.help": "MCP 服务(给 agent 的常驻通道)",
|
|
43
|
+
"cli.editor.help": "意图编辑器(给人的 web 常驻服务)",
|
|
44
|
+
"cli.option.lang.help": "界面/消息语言 zh/en(也可用环境变量 GBC_LANG)",
|
|
45
|
+
"cli.rules.help": "打印作者推荐的围栏规则集到 stdout(推荐默认,非强制沙箱)",
|
|
46
|
+
"cli.setup.help": "打印本地化的接线指南到 stdout:怎么把 MCP / skills 接入你的 agent",
|
|
47
|
+
"cli.tree.help": "把整棵 .gbc 渲染成一份 AI 可读的依赖树(gbc.md 意图为骨 + json 依赖边)",
|
|
48
|
+
"cli.tree.option.detail": "展开保证 desc/test/heavy + 每个 .gbc 目录的其它产物(.pyi)",
|
|
49
|
+
"cli.tree.option.gaps": "末尾附图反推的登记缺口(有 json/被依赖却未登记)",
|
|
50
|
+
"cli.mcp_up.arg.project_root": "目标项目根的绝对路径;省略则用当前项目判定",
|
|
51
|
+
"cli.doc.help": "意图文档(gbc.md)的合规读写入口"
|
|
52
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# GBC Recommended Guardrails (author's suggestions)
|
|
2
|
+
|
|
3
|
+
> These are **recommended defaults, not an enforced sandbox.** GBC only provides
|
|
4
|
+
> the rule text and guidance; the actual enforcement boundary must be established
|
|
5
|
+
> by your agent framework (e.g. Claude Code's `pre-tool-use` hook).
|
|
6
|
+
> **Installing GBC does NOT make you automatically safe.**
|
|
7
|
+
|
|
8
|
+
Adopting the following rules in your agent instructions (or enforcing them via your
|
|
9
|
+
framework) will markedly improve the GBC experience:
|
|
10
|
+
|
|
11
|
+
1. **The top-level agent never edits files inside `.gbc/` directly.** Intent
|
|
12
|
+
documents are changed only through the gbc-doc entry point; the guarantee graph
|
|
13
|
+
is touched only through GBC's tools (MCP / CLI). Never hand-edit anything under
|
|
14
|
+
`.gbc` — its parent/child consistency is a deterministic constraint that stays
|
|
15
|
+
correct only when maintained through the tools.
|
|
16
|
+
|
|
17
|
+
2. **Subagents touch neither `.gbc/` files nor any mutating GBC tool / gbc-doc.**
|
|
18
|
+
A subagent only implements and self-proves via `verify_*`; making commitments
|
|
19
|
+
(registering guarantees, changing intent) is reserved for the top-level agent.
|
|
20
|
+
|
|
21
|
+
3. **The top-level agent focuses on planning and aligning intent**, delegating
|
|
22
|
+
concrete coding to subagents (small changes may be done inline by the top agent).
|
|
23
|
+
|
|
24
|
+
4. **Enforce the above via your framework.** For example, Claude Code's
|
|
25
|
+
`pre-tool-use` hook can intercept a subagent's writes to `.gbc/`, or block
|
|
26
|
+
unauthorized tool calls.
|
|
27
|
+
|
|
28
|
+
5. **Remember the nature of the boundary.** GBC provides rule text and guidance,
|
|
29
|
+
not a security guarantee. What actually prevents overreach is your framework
|
|
30
|
+
configuration — set it up accordingly.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# GBC 推荐围栏规则集(作者建议)
|
|
2
|
+
|
|
3
|
+
> 这是**推荐默认**,不是强制沙箱。GBC 只提供规则文本与引导;真正的强制边界,
|
|
4
|
+
> 要靠你的 agent 框架(例如 Claude Code 的 `pre-tool-use` hook)来落地。
|
|
5
|
+
> **装了 GBC ≠ 自动安全。**
|
|
6
|
+
|
|
7
|
+
把下面的规则纳入你的 agent 指令(或用框架机制强制),能显著改善 GBC 的使用体验:
|
|
8
|
+
|
|
9
|
+
1. **主 agent 不直接编辑 `.gbc/` 内的文件。** 意图文档只经 gbc-doc 入口修改;
|
|
10
|
+
保证图只经 GBC 的工具(MCP / CLI)操作。绝不手编 `.gbc` 下的任何文件——
|
|
11
|
+
它的父子一致性是确定性约束,只有经工具维护才不漂移。
|
|
12
|
+
|
|
13
|
+
2. **subagent 既不碰 `.gbc/` 文件,也不碰任何修改类的 GBC 工具 / gbc-doc。**
|
|
14
|
+
subagent 只做实现,并用 `verify_*` 自证;登记保证、改意图这类"立约"动作
|
|
15
|
+
一律留给主 agent。
|
|
16
|
+
|
|
17
|
+
3. **主 agent 专注规划与对齐意图**,把具体编码任务派给 subagent
|
|
18
|
+
(小改动可由主 agent 一体完成)。
|
|
19
|
+
|
|
20
|
+
4. **用框架机制落地上述限制。** 例如 Claude Code 的 `pre-tool-use` hook 可以
|
|
21
|
+
拦截 subagent 对 `.gbc/` 的写入、或拦截未授权的工具调用。
|
|
22
|
+
|
|
23
|
+
5. **牢记边界的性质。** GBC 提供的是规则文本与引导,不是安全保证。
|
|
24
|
+
真正阻止越权的是你的框架配置,请据此设置。
|