回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复: - 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录) - tmp/(32.4 M,按接续棒命名的过程临时区) - .workbuddy/tmp/(39.5 M) - 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物) - tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留 入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与 接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、 .workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。 排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、 打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
214 lines
13 KiB
Python
214 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""索引 / 文档链路优化 · 离线落地包(2026-09-14 会话 `15ee0d4f`)
|
||
|
||
用法:
|
||
python3 apply-索引优化-20260914.py # dry-run:只验锚点命中数(≠1 即中止),不写任何文件
|
||
python3 apply-索引优化-20260914.py --apply # 打补丁(**需先持全局执行锁**)
|
||
|
||
补丁清单(对应评审 §三 的 G2/G3/G4 + ❓ 根因)
|
||
P0-2 docs-manifest.py 状态优先级改为「**档案头部(权威)→ INDEX(❓/? 视为缺)→ ?**」
|
||
+ 放宽提取正则 + norm_status() 规范化
|
||
⇒ 根治「INDEX 的 ❓ 回灌 manifest、再渲染成 ❓」的**环形锁定**
|
||
P1-1 docs-archive-index.py ① 顺序断言:manifest 比 04-调整方案/ 里最新档案旧 → 警告;`--write` 时**拒绝**(除非 --force)
|
||
② 输出「状态缺失率 + 名单」,缺失率 >10% 提示补头部(历史档案不回改,只在文末「修正」节补)
|
||
P2-1 docs-shrink-guard.py 新增 `--allow-shrink <路径子串>`:**合法重构**(如技能拆分)不误报
|
||
P2-2 CODEBUDDY.md(库内) ① 标题「四件套」→「**七件套 · 顺序不可换**」+ 顺序说明
|
||
② 约定 #2「单篇 ≤30 KB」补**标准拆法**:新增子页 + 原页留指针
|
||
③ 约定 #3「日志 ≤50 KB」补**执行者与触发点**:按月切 + 留指针 + 谁触到谁切
|
||
|
||
落地后的手工步骤(本脚本不代做,避免越界):
|
||
python3 scripts/docs-manifest.py && python3 scripts/docs-archive-index.py --write
|
||
bash scripts/docs-sync-check.sh # 对账;六件套其余项
|
||
scp(同相对目录)+ 远端 chmod 600 / chown root:root
|
||
git add <本次 5 个脚本 + 派生件> && git commit
|
||
"""
|
||
import io
|
||
import os
|
||
import sys
|
||
|
||
_here = os.path.dirname(os.path.abspath(__file__))
|
||
ROOT = _here # 向上找含 dsh-server-docs/ 的工作区根
|
||
while not os.path.isdir(os.path.join(ROOT, 'dsh-server-docs')) and os.path.dirname(ROOT) != ROOT:
|
||
ROOT = os.path.dirname(ROOT)
|
||
LIB = os.path.join(ROOT, 'dsh-server-docs')
|
||
if not os.path.isdir(os.path.join(LIB, 'scripts')): # 兜底:脚本直接放在库里跑
|
||
LIB = _here if os.path.isdir(os.path.join(_here, 'scripts')) else LIB
|
||
LIB = os.environ.get('IDXOPT_LIB') or LIB # 允许指向库外副本(用于 A/B 验证)
|
||
DRY = '--apply' not in sys.argv
|
||
|
||
|
||
def patch(rel, pairs):
|
||
"""pairs: [(old, new, 说明)];全部命中数必须为 1,否则整体中止不写。"""
|
||
p = os.path.join(LIB, rel)
|
||
if not os.path.exists(p):
|
||
raise AssertionError('❌ %s:库内不存在这个文件' % rel)
|
||
s = io.open(p, encoding='utf-8', newline='').read()
|
||
for old, new, why in pairs:
|
||
n = s.count(old)
|
||
assert n == 1, '❌ %s:锚点命中 %d 次(期望 1)→ %s' % (rel, n, why)
|
||
s = s.replace(old, new)
|
||
print(' ✓ %-26s %d 处' % (rel, len(pairs)))
|
||
if not DRY:
|
||
io.open(p, 'w', encoding='utf-8', newline='').write(s)
|
||
|
||
|
||
# ── P0-2 docs-manifest.py:状态以档案头部为权威,❓ 不回灌 ──────────────
|
||
NORM = '''
|
||
|
||
def norm_status(raw):
|
||
"""规范化档案头部的状态行 → 表里可读的一小段(≤14 字,去 ** 与尾部括注)。
|
||
|
||
⚠️ **权威方向**:档案头部是**源**,INDEX 表是**派生展示** —— 不允许把表里的 ❓ 回灌成 manifest 的 status,
|
||
否则 manifest → INDEX → manifest 形成环形锁定,状态永远修不回来(2026-09-14 实测 14 篇被锁)。
|
||
"""
|
||
t = re.sub(r'\\*{1,2}', '', raw or '').strip()
|
||
t = re.split(r'——|||\\||;|;|\\s{2,}', t)[0].strip()
|
||
t = re.sub(r'([^)]*)\\s*$', '', t).strip()
|
||
return t[:14]
|
||
'''
|
||
M_PAIRS = [
|
||
('import io, os, re, sys, json, collections\n',
|
||
'import io, os, re, sys, json, collections\n' + NORM,
|
||
'加 norm_status()'),
|
||
(" hs = re.search(r'- 状态:\\s*(\\S{1,12})', head)\n",
|
||
" hs = re.search(r'状态\\*{0,2}\\s*[::]\\s*(.{1,40})', head)\n"
|
||
" hs_val = norm_status(hs.group(1)) if hs else ''\n"
|
||
" st_idx = (index_status.get(num, {}) or {}).get('status') or ''\n"
|
||
" if st_idx in ('?', '❓', ''):\n"
|
||
" st_idx = '' # ❓/? 一律视为「缺」—— 防 INDEX↔manifest 环形锁定(2026-09-14)\n",
|
||
'放宽状态提取 + 保留 INDEX 兜底'),
|
||
(" 'status': (index_status.get(num, {}) or {}).get('status') or (hs.group(1) if hs else '?'),\n",
|
||
" 'status': hs_val or st_idx or '?',\n",
|
||
'状态优先级:头部 → INDEX → ?'),
|
||
]
|
||
|
||
# ── P1-1 docs-archive-index.py:顺序断言 + 缺失率 ────────────────────
|
||
A_PAIRS = [
|
||
(" if not os.path.exists(MANIFEST):\n"
|
||
" raise SystemExit('ERROR: 先跑 scripts/docs-manifest.py 生成 docs-manifest.json')\n",
|
||
" if not os.path.exists(MANIFEST):\n"
|
||
" raise SystemExit('ERROR: 先跑 scripts/docs-manifest.py 生成 docs-manifest.json')\n"
|
||
"\n"
|
||
" # ── 顺序断言(2026-09-14 加):派生链 manifest → 本脚本,顺序错会**静默**产出新旧混合 ──\n"
|
||
" newest, _ad = 0.0, os.path.join(ROOT, '04-调整方案')\n"
|
||
" if os.path.isdir(_ad):\n"
|
||
" for _n in os.listdir(_ad):\n"
|
||
" if _n.endswith('.md'):\n"
|
||
" try:\n"
|
||
" newest = max(newest, os.path.getmtime(os.path.join(_ad, _n)))\n"
|
||
" except OSError:\n"
|
||
" pass\n"
|
||
" stale_min = (newest - os.path.getmtime(MANIFEST)) / 60.0\n"
|
||
" if stale_min > 1:\n"
|
||
" msg = ('⚠️ 顺序警告:docs-manifest.json 比 04-调整方案/ 最新档案旧 %.0f 分钟 ⇒ 先跑 '\n"
|
||
" 'scripts/docs-manifest.py,否则本表用的是旧数据' % stale_min)\n"
|
||
" if '--write' in sys.argv and '--force' not in sys.argv:\n"
|
||
" raise SystemExit(msg + '\\n (确认要带旧数据刷新就加 --force)')\n"
|
||
" print(msg)\n",
|
||
'顺序断言'),
|
||
(" print('档案 %d 篇 | 摘要:手写/已存 %d | 机器兜底 %d | 状态缺失(❓) %d'\n"
|
||
" % (len(rows), n_sum, len(rows) - n_sum, sum(1 for v in rows.values() if '❓' in v)))\n",
|
||
" _miss = sorted([k for k, v in rows.items() if '❓' in v], key=sort_key)\n"
|
||
" print('档案 %d 篇 | 摘要:手写/已存 %d | 机器兜底 %d | 状态缺失(❓) %d(%.0f%%)'\n"
|
||
" % (len(rows), n_sum, len(rows) - n_sum, len(_miss),\n"
|
||
" 100.0 * len(_miss) / max(1, len(rows))))\n"
|
||
" if _miss:\n"
|
||
" print(' 缺失名单:%s' % ', '.join('04-' + m for m in _miss))\n"
|
||
" if len(_miss) / max(1, len(rows)) > 0.10:\n"
|
||
" print(' ⚠️ 缺失率 >10%% ⇒ **新档案**头部必须写「- 状态:…」;历史档案按「只增不改」不回改正文,'\n"
|
||
" '可在文末「修正(YYYY-MM-DD)」节补一行状态 ⇒ 下一轮由 manifest 从头部取到')\n",
|
||
'缺失率 + 名单'),
|
||
]
|
||
|
||
# ── P2-1 docs-shrink-guard.py:合法重构白名单 ───────────────────────
|
||
S_PAIRS = [
|
||
(" python3 scripts/docs-shrink-guard.py --baseline # 只建档不比对(首次)\n",
|
||
" python3 scripts/docs-shrink-guard.py --baseline # 只建档不比对(首次)\n"
|
||
" python3 scripts/docs-shrink-guard.py --allow-shrink <路径子串> # 合法重构白名单(不报警)\n",
|
||
'用法补一行'),
|
||
(" now = collect()\n old = {}\n",
|
||
" now = collect()\n"
|
||
" allow = [argv[i + 1] for i, a in enumerate(argv)\n"
|
||
" if a == '--allow-shrink' and i + 1 < len(argv)]\n"
|
||
" old = {}\n",
|
||
'解析 --allow-shrink'),
|
||
(" if old[rel] - n > DROP_MIN and (old[rel] - n) / old[rel] > DROP_RATIO:\n"
|
||
" shrunk.append((rel, old[rel], n))\n",
|
||
" if old[rel] - n > DROP_MIN and (old[rel] - n) / old[rel] > DROP_RATIO:\n"
|
||
" if any(a in rel for a in allow):\n"
|
||
" print(' ↳ 合法重构(白名单):%s %d → %d 行' % (rel, old[rel], n))\n"
|
||
" else:\n"
|
||
" shrunk.append((rel, old[rel], n))\n",
|
||
'白名单跳过报警'),
|
||
]
|
||
|
||
# ── P2-2 CODEBUDDY.md(库内):标题 + 约定 #2/#3 执行者 ────────────────
|
||
C_PAIRS = [
|
||
("## 改完必跑(四件套)\n",
|
||
"## 改完必跑(七件套 · **顺序不可换**)\n\n"
|
||
"> ⚠️ **顺序**:`docs-manifest.py` → `docs-archive-index.py`(**后者读前者**)→ 其余任意。\n"
|
||
"> 顺序错会**静默**产出新旧混合(2026-09-14 实测:只跑 manifest 未跑 archive-index 时,后者只读模式直接报\n"
|
||
"> 「表内容与 INDEX.md 不一致」)。`docs-archive-index.py` 已内置顺序断言:`--write` 时若 manifest 比最新档案旧会**拒绝**,\n"
|
||
"> 确认要硬刷才加 `--force`。\n",
|
||
'标题 + 顺序说明'),
|
||
("| 2 | **单篇 ≤ 30 KB**;新增档案超限**必须拆子页** | 单篇最大已 89 KB(档案 82),读一篇就吃满预算 | 写档案的人;`docs-manifest.py` 会报 |",
|
||
"| 2 | **单篇 ≤ 30 KB**;新增档案超限**必须拆子页**。**标准拆法 = 新增子页 + 原页留指针**(历史档案按「只增不改」不回改正文,只在文末「修正(YYYY-MM-DD)」节加指针) | 单篇最大已 89 KB(档案 82),读一篇就吃满预算 | 写档案的人;`docs-manifest.py` 会报 + `docs-archive-index.py` 报缺失率 |",
|
||
'约定 #2 补标准拆法'),
|
||
("| 3 | **日志按主题/月分片**,单文件 ≤ 50 KB(`append-only` 不变) | 单日日志已 377 KB | 写日志的人 |",
|
||
"| 3 | **日志按月分片**,单文件 ≤ 50 KB(`append-only` 不变):`YYYY-MM.md` → 超限后新开 `YYYY-MM-下.md`,**原文件顶部留一行指针** | 单日日志已 377 KB | **谁当月第一次触到 50 KB 谁切**(下一位写入者先看行数) |",
|
||
'约定 #3 补触发点与执行者'),
|
||
]
|
||
|
||
|
||
# ── P2-3 CODEBUDDY.md(库内)· 锁章程:补「锁的生命周期」────────────────
|
||
C2_PAIRS = [
|
||
("- 完工:`bash scripts/handoff-guard.sh --release <单号>` → 最后 `--release-exec`。\n",
|
||
"- 完工:`bash scripts/handoff-guard.sh --release <单号>` → 最后 `--release-exec`。\n"
|
||
"- 🔒 **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令):**抢到锁的任务,只有\"执行完成 → 反序释放\"才算完成**;\n"
|
||
" ⛔ **禁止\"抢到锁、做一半、不解锁就结束回合 / 结束会话\"** —— 锁是独占资源,本库**无心跳机制** ⇒ 别人既等不到也判不出你死没死(会被迫空等,或被诱去违规接管 · R9)。\n"
|
||
" 三条配套:① **抢锁前先列出收口步骤**(落地 → 校验 → 推送/对账 → 收尾);② **中途必须停**(等用户拍板 / 等窗口)⇒ **先释放再停**;\n"
|
||
" ③ **结束语必须对锁状态负责** —— 写明\"已释放\",或**显式点名**\"锁仍在 `<OWNER>` + 原因 + 下一步\"(仅限释放通道不可用);⛔\"忘了 / 做不完就走\"不允许。\n",
|
||
'库内 CODEBUDDY 补锁生命周期'),
|
||
]
|
||
|
||
# ── P2-4 handoff-guard.sh:抢锁成功时当场把规矩打给持有者(机制层最有效)──
|
||
H_PAIRS = [
|
||
(' echo "✓ 已持全局执行锁($OWNER)—— 完工请 --release-exec"\n',
|
||
' echo "✓ 已持全局执行锁($OWNER)"\n'
|
||
' echo " ⛔ **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令):执行完成 → 必须 `--release-exec` 才算完成;"\n'
|
||
' echo " 禁止抢锁做一半、不解锁就结束回合/会话(本库无心跳,带锁结束 = 把所有人挡在门外)。中途要停 ⇒ 先释放再停。"\n',
|
||
'抢锁输出加硬提示'),
|
||
]
|
||
|
||
|
||
def main():
|
||
print('落地包(%s)|库根 = %s' % ('dry-run' if DRY else '**APPLY**', LIB))
|
||
for rel, pairs in (('scripts/docs-manifest.py', M_PAIRS),
|
||
('scripts/handoff-guard.sh', H_PAIRS),
|
||
('CODEBUDDY.md', C2_PAIRS),
|
||
('scripts/docs-archive-index.py', A_PAIRS),
|
||
('scripts/docs-shrink-guard.py', S_PAIRS),
|
||
('CODEBUDDY.md', C_PAIRS)):
|
||
try:
|
||
patch(rel, pairs)
|
||
except AssertionError as e:
|
||
print(' %s' % e)
|
||
print('\n⛔ 已中止,**未写入任何文件**(逐文件原子:前面已写的不会回滚,请先查清冲突)')
|
||
return 1
|
||
if DRY:
|
||
print('\n(dry-run 通过)加 --apply 落盘。')
|
||
else:
|
||
print('\n✅ 已落盘。接着手工执行:')
|
||
print(' python3 scripts/docs-manifest.py')
|
||
print(' python3 scripts/docs-archive-index.py --write')
|
||
print(' python3 scripts/docs-audit.py && python3 scripts/docs-consistency.py '
|
||
'&& bash scripts/docs-sync-check.sh')
|
||
print(' scp(同相对目录)+ 远端 chmod 600 / chown root:root')
|
||
print(' git add scripts/docs-*.py CODEBUDDY.md INDEX.md archive-summaries.json docs-manifest.json && git commit')
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
raise SystemExit(main())
|