#!/usr/bin/env python3 # -*- coding: utf-8 -*- """resident-rules.py —— 「常驻规则」的可移植化:快照 / 校验 / 注入 / 环境自检 为什么需要它 ──────────── 项目常驻规则写在**工作区**的 `CODEBUDDY.md`(每会话自动注入 ⇒ 动作前必然生效)。 但它是**工作区文件**:换电脑 / 换工作区路径 ⇒ 规则直接消失,而"技能"才是**可移植层**。 本脚本 + 同目录 `references/常驻规则-快照.md` 把两者接起来: CODEBUDDY.md(工作区·权威·动作前生效) │ --snapshot(抽取 §1/§3/§4/§5/§6/§8 全文) ▼ references/常驻规则-快照.md(技能内·可移植·随技能走) │ --check / --inject(到新环境的 CODEBUDDY.md) ▼ 新环境的 CODEBUDDY.md 设计红线(避免造"第二真相源") ────────────────────────────── 1. **权威方向单向**:`CODEBUDDY.md` 是权威,快照是**它的副本**(`--snapshot` 生成,不手改)。 2. **默认只报不改**:`--check` 只报告差异(退出码 1)—— 与文档库「体检只报不改」同规。 3. **注入只动标记块**:`--inject` 只替换 ` … ` 之间的内容; 首次注入**不自动建块**(除非 `--init`),避免与人工撰写的内容重复成两处。 4. **区分环境无关 / 环境相关**:环境相关项(绝对路径 / IP / hooks 路径 / 工具位置) 注入后**必须按新环境核对** —— 脚本给「待核清单」,不假装能自动配好。 用法 ──── python3 resident-rules.py --snapshot # 由 CODEBUDDY.md 重生成技能内快照 python3 resident-rules.py --check [--goal ] # 校验目标环境(默认只报,rc=1 = 有差异) python3 resident-rules.py --env-check # 环境自检:路径 / hooks / 锁脚本 / 工作区 python3 resident-rules.py --inject [--goal ] [--init] # 注入(需已存在标记块) 退出码:0 = 一致/成功;1 = 有差异(未改任何文件);2 = 用法/环境异常 """ import io import json import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) SKILL = os.path.dirname(HERE) SNAP = os.path.join(SKILL, 'references', '常驻规则-快照.md') DEFAULT_GOAL = r'E:/ProgramData/AI技能/aliyun-dsh-server/CODEBUDDY.md' # 要随技能走的章节("动作前必须生效"的那些 + 事故级事实) SECTIONS = ['## 1. 提问判据', '## 3. 红线', '## 4. 提交边界', '## 5. 规划与执行分离', '## 6. 并发纪律', '## 8. '] # ★骨架锚点:这几条丢了就等于规则没了 —— 校验时逐条查存在性(短语取自权威文件) ANCHORS = [ ('A1', '只问「超过现有判断方法边界」的问题'), ('A2', '禁止用征询句收尾'), ('A3', '四件或七件套必跑', '占位'), # 由下方 canary 覆盖 ('R7', '禁未经确认的批量 / 全仓写入'), ('R7-边界', 'R7 只适用于「不是我的 lane」'), ('R9', '绝对禁止「人工删锁 / 接管」'), ('R11', '只做正向迭代'), ('U27', '目标不打折,路径取最小代价'), ('L1', '先抢全局执行锁'), ('L2', '锁的生命周期 = 任务的生命周期'), ] MARK_BEGIN = '' MARK_END = '' # 环境相关项:注入/换机后**必须按新环境核对**(不是"自动配好") ENV_ITEMS = [ ('工作区根', r'E:/ProgramData/AI技能/aliyun-dsh-server'), ('文档库', r'D:/github/dsh_shenxian/dsh-server-docs'), ('代码仓', r'D:/github/dsh_shenxian'), ('备份目录', r'/opt/dsh/backups'), ('服务器', 'bt-server(47.77.182.89,SSH 32022)'), ] def rd(p): try: return io.open(p, encoding='utf-8', newline='').read() except OSError: return '' def section_of(text, head): """取 `head` 章节到下一个同级 `## ` 之间的内容(含标题行)。""" i = text.index(head) m = re.search(r'^## ', text[i + len(head):], re.M) return text[i:i + len(head) + (m.start() if m else len(text) - i - len(head))].rstrip() + '\n' def snapshot(src): t = rd(src) if not t: raise SystemExit('ERROR: 读不到源文件 %s' % src) parts = [] for h in SECTIONS: if h == '## 3. 红线': h = '## 3. 红线 R1' # 实际标题含编号,用前缀匹配 i = t.find(h) if i < 0: continue j = t.find('\n## 4.', i) parts.append(t[i:j if j > 0 else len(t)].rstrip() + '\n') continue if t.find(h) < 0: parts.append('\n' % h) continue parts.append(section_of(t, h)) body = '\n---\n\n'.join(parts) head = ('# 常驻规则 · 可移植快照(由 `scripts/resident-rules.py --snapshot` 生成,勿手改)\n\n' '> **这是「工作区 `CODEBUDDY.md` 关键章节」的副本**,随技能走 ⇒ 换电脑 / 换工作区也能带走。\n' '> ⚠️ **权威方向单向**:`CODEBUDDY.md` 是权威,本文件是它的副本(重生成用 `--snapshot`)。\n' '> ⚠️ **环境相关项**(绝对路径 / IP / hooks 路径 / 工具位置)注入后**必须按新环境核对**:见文末「待核清单」。\n\n' '## 骨架锚点(校验用;丢了就等于规则没了)\n\n') anch = '\n'.join('- `%s` — %s' % (k, v) for k, v, *_ in [(a[0], a[1]) for a in ANCHORS]) + '\n' tail = ('\n---\n\n## 待核清单(换环境后逐项核对,脚本不代改)\n\n' + '\n'.join('- [ ] **%s** %s' % (k, v) for k, v in ENV_ITEMS) + '\n') out = head + anch + '\n---\n\n' + body + tail os.makedirs(os.path.dirname(SNAP), exist_ok=True) io.open(SNAP, 'w', encoding='utf-8', newline='\n').write(out) return len(out), len(parts) def check(goal): t = rd(goal) if not t: print('ERROR: 读不到目标 %s' % goal) return 2 snap = rd(SNAP) if not snap: print('ERROR: 缺少技能内快照(先跑 --snapshot)') return 2 print('=== 常驻规则校验|目标 = %s ===' % goal) bad = 0 for a in ANCHORS: k, phrase = a[0], a[1] if phrase.startswith('四件或七件套'): ok = ('七件套' in t) or ('四件套' in t) else: ok = phrase in t print(' %-10s %s %s' % (k, '✓' if ok else '✗ 缺失', phrase[:40])) bad += 0 if ok else 1 sec_ok = sum(1 for h in ('## 1. 提问判据', '## 3. 红线', '## 4. 提交边界', '## 5. 规划与执行分离', '## 6. 并发纪律') if h in t) print(' 章节存在性:%d/5' % sec_ok) bad += 5 - sec_ok print('结论:%s' % ('✅ 关键规则齐备' if bad == 0 else '⚠️ 有 %d 处缺失/漂移 ⇒ 用 --inject(或人工补齐)' % bad)) return 1 if bad else 0 def env_check(): print('=== 环境自检(换机后逐项确认)===') bad = 0 for k, v in ENV_ITEMS: if k in ('服务器', '备份目录'): print(' %-8s %-52s (需 ssh 侧核对)' % (k, v)) continue ex = os.path.exists(v) print(' %-8s %-52s %s' % (k, v, '✓ 存在' if ex else '✗ 不存在')) bad += 0 if ex else 1 # hooks 里的绝对路径(工作区搬迁后最常见的坑) st = r'E:/ProgramData/.workbuddy/settings.json' if not os.path.exists(st): st = os.path.expanduser('~/.workbuddy/settings.json') try: h = json.loads(rd(st)).get('hooks', {}) except Exception: h = {} cmds = [x.get('command', '') for v in h.values() for it in v for x in it.get('hooks', []) if x.get('type') == 'command'] print(' hooks 命令 %d 条:' % len(cmds)) for c in cmds: paths = re.findall(r'[A-Za-z]:/[^\s"]+?\.(?:py|sh|exe)', c) miss = [q for q in paths if not os.path.exists(q.replace('/', os.sep))] flag = '✗ 路径失效' if miss else '✓' print(' %s %s' % (flag, c[:100])) bad += 1 if miss else 0 return 1 if bad else 0 def inject(goal, init=False): t = rd(goal) if not t: print('ERROR: 读不到目标 %s' % goal) return 2 snap = rd(SNAP) if not snap: print('ERROR: 缺少技能内快照(先跑 --snapshot)') return 2 body = snap.split('## 待核清单', 1)[0] block = MARK_BEGIN + '\n' + body.strip() + '\n' + MARK_END + '\n' if MARK_BEGIN in t: new = re.sub(re.escape(MARK_BEGIN) + r'.*?' + re.escape(MARK_END) + r'\n?', block, t, flags=re.S) io.open(goal, 'w', encoding='utf-8', newline='').write(new) print('✓ 已替换标记块内内容(块外人工内容未动)') return 0 if not init: print('⚠️ 目标里没有标记块 ⇒ **不自动注入**(避免与人工撰写的内容重复成两处)。\n' ' 要建立标记块并注入:加 --init(会**追加**到目标文末,不动既有内容)') return 1 io.open(goal, 'w', encoding='utf-8', newline='').write(t.rstrip('\n') + '\n\n' + block) print('✓ 已追加标记块(既有内容未动);建议随后人工去重并 `--check`') return 0 def main(argv): goal = DEFAULT_GOAL if '--goal' in argv: goal = argv[argv.index('--goal') + 1] if '--snapshot' in argv: n, k = snapshot(goal) print('✓ 已由 %s 生成快照:%d 字符 / %d 个章节 → %s' % (goal, n, k, os.path.relpath(SNAP, SKILL))) return 0 if '--env-check' in argv: return env_check() if '--inject' in argv: return inject(goal, init=('--init' in argv)) if '--check' in argv: return check(goal) print(__doc__) return 2 if __name__ == '__main__': raise SystemExit(main(sys.argv[1:]))