1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
123 lines
5.2 KiB
Python
123 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""docs-shrink-guard.py — 防「共享文件被整文件重写抹掉别人的行」
|
||
|
||
为什么需要(2026-09-14 实测)
|
||
* `MEMORY.md` 被并行会话**整文件重写**,抹掉了本会话刚写进去的两行;
|
||
* 本库的规矩是「共享文件只用 Edit 精确替换,禁整文件 Write」—— 但**只靠人守纪律**,没有机制兜底。
|
||
⇒ 本脚本给"纪律"加一条**机械兜底**:记录每个文件的**行数快照**,下次运行时若某文件
|
||
**行数骤降**(默认 >30% 且绝对减少 >20 行),就把它当成"疑似被整文件重写"报警。
|
||
|
||
覆盖范围(两处共享热区):
|
||
* 文档库 `dsh-server-docs/**/*.md`
|
||
* 工作区记忆 `.workbuddy/memory/*.md`
|
||
快照落在**库外**(`<工作区>/.workbuddy/cache/docs-lines.json`)—— 放库内会污染 `docs-sync-check` 对账。
|
||
|
||
用法
|
||
python3 scripts/docs-shrink-guard.py # 比对并报告(不改快照)
|
||
python3 scripts/docs-shrink-guard.py --write # 比对 + 刷新快照(改完文件后跑)
|
||
python3 scripts/docs-shrink-guard.py --baseline # 只建档不比对(首次)
|
||
python3 scripts/docs-shrink-guard.py --allow-shrink <路径子串> # 合法重构白名单(不报警)
|
||
退出码:0 = 无骤降;1 = 发现骤降(需人看一眼 diff);2 = 用法错误
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
DOCS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
WS = os.path.dirname(DOCS)
|
||
SNAP = os.path.join(WS, '.workbuddy', 'cache', 'docs-lines.json')
|
||
DROP_RATIO, DROP_MIN = 0.30, 20
|
||
|
||
|
||
def collect():
|
||
out = {}
|
||
roots = [(DOCS, 'docs'), (os.path.join(WS, '.workbuddy', 'memory'), 'memory')]
|
||
for root, tag in roots:
|
||
if not os.path.isdir(root):
|
||
continue
|
||
for base, dirs, names in os.walk(root):
|
||
if '.git' in dirs:
|
||
dirs.remove('.git')
|
||
if os.sep + 'cache' in base:
|
||
continue
|
||
for n in sorted(names):
|
||
if not n.endswith('.md') or '.bak' in n:
|
||
continue
|
||
p = os.path.join(base, n)
|
||
rel = os.path.relpath(p, WS).replace('\\', '/')
|
||
try:
|
||
out[rel] = sum(1 for _ in io.open(p, encoding='utf-8', errors='replace'))
|
||
except OSError:
|
||
continue
|
||
return out
|
||
|
||
|
||
def main(argv):
|
||
now = collect()
|
||
allow = [argv[i + 1] for i, a in enumerate(argv)
|
||
if a == '--allow-shrink' and i + 1 < len(argv)]
|
||
old = {}
|
||
if os.path.exists(SNAP):
|
||
try:
|
||
old = json.loads(io.open(SNAP, encoding='utf-8').read())
|
||
except ValueError:
|
||
print('⚠️ 快照损坏,按首次处理')
|
||
if '--baseline' in argv or not old:
|
||
if '--write' in argv or '--baseline' in argv:
|
||
os.makedirs(os.path.dirname(SNAP), exist_ok=True)
|
||
io.open(SNAP, 'w', encoding='utf-8', newline='\n').write(
|
||
json.dumps(now, ensure_ascii=False, indent=1, sort_keys=True) + '\n')
|
||
print('已建基线:%d 个文件 → %s' % (len(now), SNAP))
|
||
return 0
|
||
print('无快照(首次)。跑 `--baseline` 建档后再用。')
|
||
return 2
|
||
|
||
shrunk, added, removed, grew = [], [], [], []
|
||
for rel, n in now.items():
|
||
if rel not in old:
|
||
added.append(rel)
|
||
elif n < old[rel]:
|
||
if old[rel] - n > DROP_MIN and (old[rel] - n) / old[rel] > DROP_RATIO:
|
||
if any(a in rel for a in allow):
|
||
print(' ↳ 合法重构(白名单):%s %d → %d 行' % (rel, old[rel], n))
|
||
else:
|
||
shrunk.append((rel, old[rel], n))
|
||
elif old[rel] - n > 0:
|
||
grew.append((rel, old[rel], n, 'down'))
|
||
for rel in old:
|
||
if rel not in now:
|
||
removed.append(rel)
|
||
for rel, n in now.items():
|
||
if rel in old and n > old[rel]:
|
||
grew.append((rel, old[rel], n, 'up'))
|
||
|
||
print('文件 %d(新增 %d / 删除 %d)| 行数变化 %d' % (len(now), len(added), len(removed), len(grew)))
|
||
for rel in added[:8]:
|
||
print(' + %s' % rel)
|
||
for rel in removed[:8]:
|
||
print(' - %s(消失?)' % rel)
|
||
for rel, o, n, d in sorted(grew, key=lambda x: -abs(x[1] - x[2]))[:6]:
|
||
print(' %s %s %d → %d 行' % ('↑' if d == 'up' else '↓', rel, o, n))
|
||
|
||
bad = 0
|
||
if shrunk:
|
||
bad = 1
|
||
print('\n⚠️ **疑似被整文件重写(行数骤降)** —— 请看一眼 `git diff` 或与上一位写入者核对:')
|
||
for rel, o, n in shrunk:
|
||
print(' %s %d → %d 行(-%.0f%%)' % (rel, o, n, (o - n) / o * 100))
|
||
print(' 本库规矩:共享文件只用 Edit 精确替换,禁整文件 Write(见 CODEBUDDY.md)')
|
||
if '--write' in argv:
|
||
os.makedirs(os.path.dirname(SNAP), exist_ok=True)
|
||
io.open(SNAP, 'w', encoding='utf-8', newline='\n').write(
|
||
json.dumps(now, ensure_ascii=False, indent=1, sort_keys=True) + '\n')
|
||
print('已刷新快照。')
|
||
elif bad:
|
||
print('(未刷新快照:确认无误后跑 `--write`)')
|
||
return bad
|
||
|
||
|
||
if __name__ == '__main__':
|
||
raise SystemExit(main(sys.argv[1:]))
|