#!/usr/bin/env python3 """ fix_i18n_w1_dspy.py — dspy 安全修复 W1(保守策略) 只处理纯字面量 "text": "中文"(match 后第一个非空白字符是 , 或 }), 跳过所有拼接(+)、多行隐式连接(后跟换行+字符串)、变量插值——那些记入 manual 清单。 替换: "text": "X" → "otext": "X", "text": "X", "i18n": True 引号风格跟随原文(' 或 ")。 用法: python3 scripts/fix_i18n_w1_dspy.py /tmp/i18n.json [--dry-run] """ import json, re, sys from collections import defaultdict TEXT_RE = re.compile(r'''(['"])text\1\s*:\s*(['"])((?:[^'"\\]|\\.)*)\2''') def safe_literal(raw, m): """match 后第一个非空白字符必须是 , 或 }(排除拼接与多行隐式连接)""" j = m.end() while j < len(raw) and raw[j] in ' \t': j += 1 return j < len(raw) and raw[j] in ',}' def main(): report = json.load(open(sys.argv[1], encoding='utf-8')) dry = '--dry-run' in sys.argv viols = [v for v in report['violations'] if v['code'] == 'W1' and not v['git_ignored'] and v['file'].endswith('.dspy')] by_file = defaultdict(list) for v in viols: by_file[v['file']].append(v) fixed = manual = 0 manual_list = [] for fp, vs in sorted(by_file.items()): raw = open(fp, encoding='utf-8').read() # 从后往前 changed = False for v in sorted(vs, key=lambda x: -x['pos']): pos = v['pos'] m = TEXT_RE.match(raw, pos) if not m: # otext 无 i18n 的情形 mo = re.compile(r'''(['"])otext\1\s*:\s*(['"])((?:[^'"\\]|\\.)*)\2''').match(raw, pos) if mo and not re.search(r"['\"]i18n['\"]\s*:\s*True", raw[mo.end():mo.end()+60]): if safe_literal(raw, mo): ins = f', {mo.group(1)}i18n{mo.group(1)}: True' if not dry: raw = raw[:mo.end()] + ins + raw[mo.end():] fixed += 1; changed = True else: manual += 1; manual_list.append((fp, pos, 'otext动态', v['text'][:50])) else: manual += 1; manual_list.append((fp, pos, '无法定位', v['text'][:50])) continue val = m.group(3) if not safe_literal(raw, m): manual += 1 manual_list.append((fp, pos, '拼接/多行串', val[:50])) continue q1, q2 = m.group(1), m.group(2) seg = raw[m.start():m.end()] new_seg = f'{q1}otext{q1}: {q2}{val}{q2}, {q1}text{q1}: {q2}{val}{q2}, {q1}i18n{q1}: True' if not dry: raw = raw[:m.start()] + new_seg + raw[m.end():] fixed += 1; changed = True if changed and not dry: open(fp, 'w', encoding='utf-8').write(raw) print(f'{"DRY-RUN " if dry else ""}fixed={fixed} manual={manual} files={len(by_file)}') for mm in manual_list[:40]: print(' MANUAL', mm[0].split('/repos/')[-1], f'@{mm[1]}', mm[2], '|', mm[3]) if __name__ == '__main__': main()