Files

122 lines
5.2 KiB
Python
Raw Permalink Normal View History

#!/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 07-scripts/docs-shrink-guard.py # 比对并报告(不改快照)
python3 07-scripts/docs-shrink-guard.py --write # 比对 + 刷新快照(改完文件后跑)
python3 07-scripts/docs-shrink-guard.py --baseline # 只建档不比对(首次)
python3 07-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:]))