#!/usr/bin/env python3 # -*- coding: utf-8 -*- """docs-archive-index.py — 让 INDEX.md 的**档案清单表**变成派生件(根治"漏登记") 背景(2026-09-14 实测):档案清单**靠手写**,已漏 82–88 共 7 篇;而 `docs-manifest.py` 已经能机读全部档案(号/状态/tier/域/tldr)。本脚本把两者接上: docs-manifest.json ──┐ archive-summaries.json ─┴─→ INDEX.md §二 的档案表(原地替换,不搬家) 设计要点 1. **不丢手写内容**:首次运行会把现有表里手写的「一句话」**抽取到 `archive-summaries.json`** (人工可编辑的映射文件);之后表的摘要优先级 = summaries → manifest.tldr → 标题。 2. **原地替换**:只替换 `| 04-NN | … |` 那一段连续行,**表头与根级编号行(01/02/03/06)不动**; 块边界用 BEGIN/END 注释标记,第二次起按标记整块重生成。 3. **缺失可见**:状态取不到显 `❓`、摘要取不到显 `—` —— 让"没写好头部"的档案**在表里看得见**。 用法: python3 07-scripts/docs-archive-index.py # 只打印(不写任何文件) python3 07-scripts/docs-archive-index.py --write # 写 archive-summaries.json + INDEX.md 退出码:0 = 一致或已刷新;1 = 有差异且未加 --write;2 = 结构异常 """ import io import json import os import re import sys ROOT = sys.argv[1] if len(sys.argv) > 1 and not sys.argv[1].startswith('-') else \ os.path.dirname(os.path.dirname(os.path.abspath(__file__))) INDEX = os.path.join(ROOT, 'INDEX.md') SUMS = os.path.join(ROOT, 'archive-summaries.json') MANIFEST = os.path.join(ROOT, 'docs-manifest.json') B = '' E = '' HEADER = ['| 号 | 状态 | 一句话(**机器生成**;摘要存 `archive-summaries.json`)|', '|---|---|---|'] ARCH_RE = re.compile(r'^\|\s*04-(\d{1,3}[a-z]?)\s*\|\s*(\S+)\s*\|\s*(.*?)\s*\|\s*$') ROW_RE = re.compile(r'^\|\s*04-(\d{1,3}[a-z]?)\s*\|') # 宽松:也认手写表的坏行(缺尾竖线/带 CR) def rd(path): try: return io.open(path, encoding='utf-8', newline='').read() except OSError: return '' def sort_key(num): return (int(re.sub(r'\D', '', num) or 0), num) def harvest(index_text, existing): """把现有表里手写的「一句话」抽进 summaries(只补空缺,不覆盖已有)。""" out = dict(existing) for line in index_text.split('\n'): m = ARCH_RE.match(line) if m and m.group(1) not in out: out[m.group(1)] = m.group(3) return out def body(manifest, sums): rows = {} for i in sorted([x for x in manifest['items'] if x.get('num')], key=lambda x: sort_key(x['num'])): num = i['num'] status = i['status'] if i['status'] not in ('?', '') else '❓' text = (sums.get(num) or i.get('tldr') or re.sub(r'^\d{1,3}[a-z]?-', '', i.get('title') or '').strip() or '—') rows[num] = '| 04-%s | %s | %s |' % (num, status, text[:110]) return rows def splice(index_text, gen): """逐行替换(表里 04-* 行与 `—`/根级行是交错的),并按号补插缺失档案。""" lines = index_text.split('\n') nums = sorted(gen, key=sort_key) out, used, dup = [], set(), [] def emit_before(limit): for n in nums: if n not in used and (limit is None or sort_key(n) < sort_key(limit)): out.append(gen[n]) used.add(n) for line in lines: m = ROW_RE.match(line) if m is None: out.append(line) continue emit_before(m.group(1)) if m.group(1) in gen: if m.group(1) not in used: # 首次出现 → 用生成行 out.append(gen[m.group(1)]) used.add(m.group(1)) else: # 重复的手写行 → 丢弃(自动去重) dup.append((m.group(1), line[:60])) else: out.append(line) # 表里独有的号(如空号)→ 保留 emit_before(None) if dup: print(' 自动丢弃重复行 %d 条:%s' % (len(dup), [d[0] for d in dup])) for n in nums: if n not in used: out.append(gen[n]) return '\n'.join(out) def main(): if not os.path.exists(MANIFEST): raise SystemExit('ERROR: 先跑 07-scripts/docs-manifest.py 生成 docs-manifest.json') # ── 顺序断言(2026-09-14 加):派生链 manifest → 本脚本,顺序错会**静默**产出新旧混合 ── newest, _ad = 0.0, os.path.join(ROOT, '04-调整方案') if os.path.isdir(_ad): for _n in os.listdir(_ad): if _n.endswith('.md'): try: newest = max(newest, os.path.getmtime(os.path.join(_ad, _n))) except OSError: pass stale_min = (newest - os.path.getmtime(MANIFEST)) / 60.0 if stale_min > 1: msg = ('⚠️ 顺序警告:docs-manifest.json 比 04-调整方案/ 最新档案旧 %.0f 分钟 ⇒ 先跑 ' '07-scripts/docs-manifest.py,否则本表用的是旧数据' % stale_min) if '--write' in sys.argv and '--force' not in sys.argv: raise SystemExit(msg + '\n (确认要带旧数据刷新就加 --force)') print(msg) manifest = json.loads(rd(MANIFEST)) index_text = rd(INDEX) old = {} if os.path.exists(SUMS): try: old = json.loads(rd(SUMS)) except ValueError: raise SystemExit('ERROR: archive-summaries.json 不是合法 JSON') sums = harvest(index_text, old) rows = body(manifest, sums) new_text = splice(index_text, rows) changed = new_text != index_text or sums != old n_sum = sum(1 for k in rows if sums.get(k)) _miss = sorted([k for k, v in rows.items() if '❓' in v], key=sort_key) print('档案 %d 篇 | 摘要:手写/已存 %d | 机器兜底 %d | 状态缺失(❓) %d(%.0f%%)' % (len(rows), n_sum, len(rows) - n_sum, len(_miss), 100.0 * len(_miss) / max(1, len(rows)))) if _miss: print(' 缺失名单:%s' % ', '.join('04-' + m for m in _miss)) if len(_miss) / max(1, len(rows)) > 0.10: print(' ⚠️ 缺失率 >10%% ⇒ **新档案**头部必须写「- 状态:…」;历史档案按「只增不改」不回改正文,' '可在文末「修正(YYYY-MM-DD)」节补一行状态 ⇒ 下一轮由 manifest 从头部取到') if '--write' in sys.argv: io.open(SUMS, 'w', encoding='utf-8', newline='\n').write( json.dumps(dict(sorted(sums.items(), key=lambda kv: sort_key(kv[0]))), ensure_ascii=False, indent=1) + '\n') if new_text != index_text: io.open(INDEX, 'w', encoding='utf-8', newline='').write(new_text) print('已刷新 INDEX.md 档案表 + archive-summaries.json') else: print('INDEX.md 已是最新(仅刷新 archive-summaries.json)') return 0 print('(只读模式)表内容与 INDEX.md %s' % ('一致' if not changed else '不一致,加 --write 刷新')) return 1 if changed else 0 if __name__ == '__main__': raise SystemExit(main())