Files
dsh_shenxian/dsh-server-docs/08-skills/dsh-local-env/references/dsh-env-bootstrap/resident-rules.py
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

270 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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` 只替换 `<!-- BEGIN resident-rules … --> … <!-- END -->` 之间的内容;
首次注入**不自动建块**(除非 `--init`),避免与人工撰写的内容重复成两处。
4. **区分环境无关 / 环境相关**:环境相关项(绝对路径 / IP / hooks 路径 / 工具位置)
注入后**必须按新环境核对** —— 脚本给「待核清单」,不假装能自动配好。
用法
────
python3 resident-rules.py --snapshot # 由 CODEBUDDY.md 重生成技能内快照
python3 resident-rules.py --check [--goal <CODEBUDDY.md>] # 校验目标环境(默认只报,rc=1 = 有差异)
python3 resident-rules.py --env-check # 环境自检:路径 / hooks / 锁脚本 / 工作区
python3 resident-rules.py --inject [--goal <CODEBUDDY.md>] [--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)
# 🔴 2026-10-02 修(**同款事故复发**:路径靠"层级"推导 ⇒ 静默写到错地方,校验还全绿)
# 事故:本脚本实际在 `<技能>/references/dsh-env-bootstrap/`,**比设计假设多一层** ⇒
# `SNAP = SKILL/references/…` 算成 `…/dsh-local-env/references/references/常驻规则-快照.md`
# (**双层 references**)⇒ `--check` 报「缺少快照」、`--snapshot` 则会在别处**新建一份**,
# 而真身留在 `bootstrap/` 里 ⇒ **两份快照开始分叉**,且两边看起来都正常。
# 🔴 与 `wb-result-hook.py` 那次(脚本一移,`dirname(dirname())` 就指错 ⇒ 台账静默写到别处,
# 而"测试全绿、rc=0")**是同一个病** ⇒ 教训:**路径靠内容标志锚定,⛔ 绝不靠脚本在第几层**。
# ⇒ 现在:**认内容,不认层级** —— 本目录找同名快照;没有就上溯找;都没有才把新快照落**本目录**。
_SNAP_NAME = '常驻规则-快照.md'
def _resolve_snap():
"""快照路径=**认内容**(同名文件),⛔ 不认"我在第几层"。返回找到的路径,或 None。"""
cur = HERE
for _ in range(5): # 覆盖 skills/<name>/references/<sub>/ 等实际摆法
for cand in (os.path.join(cur, _SNAP_NAME),
os.path.join(cur, 'references', _SNAP_NAME)):
if os.path.isfile(cand):
return cand
parent = os.path.dirname(cur)
if parent == cur:
break
cur = parent
return None # 真没有 ⇒ 报「缺少快照」,由 --snapshot 在本目录新建
_SNAP_FOUND = _resolve_snap()
# 找不到 ⇒ 落**本脚本同目录**(⛔ 不再是双层 references),生成后与本脚本同处,最省心
SNAP = _SNAP_FOUND or os.path.join(HERE, _SNAP_NAME)
DEFAULT_GOAL = r'E:/ProgramData/AIProject/ai1net-dsh-server/CODEBUDDY.md'
# 要随技能走的章节("动作前必须生效"的那些 + 事故级事实)
SECTIONS = ['## 1. 提问判据', '## 3. 红线', '## 4. 提交边界', '## 5. 规划与执行分离',
'## 6. 并发纪律', '## 8. ']
# ★骨架锚点:这几条丢了就等于规则没了 —— 校验时逐条查存在性
#
# 🔴 2026-10-02 改(**假红真因**):原表是**整句字面匹配**,而权威文件会改写措辞 ⇒
# 反复报「✗ 缺失」→ 诱导人**反复重生成快照**,⛔ 而重生成解决不了关键词不匹配(纯属白忙)。
# 实测:权威写 `R7 只管`(表里 `只适用于`)/ 锁以 `域锁…不带 --domains 退化独占` 表达
# (表里 `先抢全局执行锁`)⇒ 四条假红,**而这些规则实际都在**。
# ⇒ 修法:**每条锚点改成「稳定语义锚」**(选不会被改写掉的那个词/标记),⛔ 不再匹配整句。
# 仍认不到的(如 U27 权威侧确实没有)**照实报缺** —— 宁可少一条,⛔ 也不能把"规则在"报成"不在"。
ANCHORS = [
('A1', '超过现有判断方法边界'),
('A2', '征询句'),
('A3', '四件或七件套必跑', '占位'), # 由下方 canary 覆盖
('R7', '禁未经确认的批量'),
# 🔴 `R7 只管` ⇄ 旧 `只适用于` 是同一规则的两种写法 ⇒ 锚点取**两者共有的**「不是我的 lane」
('R7-边界', '不是我的 lane'),
('R9', '人工删锁'),
('R11', '正向迭代'),
# 🔴 U27「目标不打折,路径取最小代价」在权威文件里**换了编号与措辞**:旧编号 U27 ⇒ 现行
# `要解决问题,不将就妥协`(降级/延期/静默兜底都不算解决)+ R11。**实质规则仍在**,
# ⛔ 不是被删了 ⇒ 锚到现行那句的稳定词,⛔ 不靠放宽判据把它"弄绿"。
('U27', '不将就妥协'),
# 🔴 现行锁模型=「域锁为主 / 不带 --domains ⇒ 退化独占」;旧口径叫「全局执行锁」⇒ 锚到真正稳定的那句
('L1', '退化独占'),
('L2', '锁的生命周期'),
]
MARK_BEGIN = '<!-- BEGIN resident-rules (generated by skills/dsh-env-bootstrap · 勿手改块内) -->'
MARK_END = '<!-- END resident-rules -->'
# 环境相关项:注入/换机后**必须按新环境核对**(不是"自动配好")
ENV_ITEMS = [
('工作区根', r'E:/ProgramData/AIProject/ai1net-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 **22**;别名里的 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('<!-- 源文件里没有 `%s` 章节 -->\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:]))