from pathlib import Path
import json
import html

root = Path(__file__).resolve().parent
base = json.loads((root/'issues-baseline.json').read_text())
coverage = json.loads((root/'coverage.json').read_text())
original = {x['id']:x for x in base['formal']}
cases = {
 'AUDIO-N01':('已发布无音频词有在途TTS；人工上传音频、修改释义仅保存；恢复TTS完成。', '人工草稿保持待发布；PROD名称/翻译/变体不变；无草稿且当前已发布的对照可补发布。', '词条保存、补录、音频、双库发布', '两库先建outbox；注册englishDictOutboxReplay。'),
 'AUDIO-N02':('分别持续query异常、completed下载失败、running；提交时间超过1800秒，再测预算内下载失败。', '所有超时分支都结束当前尝试并记录失败；cancel异常不阻清理；预算内失败保留任务。', 'TTS状态、下载上传、超时与批次', '沿用音频回填Job与超时配置。'),
 'AUDIO-N03':('submit一直成功而每次终态failed/cancelled或超时；连续三轮；成功/人工补齐另做对照。', '失败跨尝试累计，到上限停止提交；只有成功完成才清计数。', '重试预算、Redis计数、平台配额', '失败计数7天冷却；验证真实Redis。'),
 'AUDIO-N04':('batch=1，最小ID连续失败达到上限，恢复TTS后还有第二个健康ID；变体前页均人工来源另验。', '游标继续前进，后ID能被提交；完整一圈回到起点，不被最早失败页永久占满。', '三表分页、游标、来源过滤', '新增afterId查询及Redis游标，三表独立。'),
 'AUDIO-N05':('唯一缺音频的词回填成功；先预热JY详情缓存；缺音标的词作为对照。', '字段齐全词completeness=1且RPC complete=true；教研暖缓存通过outbox失效；仍缺字段词保持0。', '音频回填事务、完整性、JY/PROD缓存', '两库outbox DDL及持续重放Job。'),
 'SEG01':('首次同步后暂停compute消费者立即发布；随后恢复消费者，再跑全量；对照已有成功行发布。', 'JY/PROD INIT身份成对，不依赖首次NORMAL才建；随后可算成NORMAL，正式库不自由自增。', '首次发布、历史缺行、ID映射、全量', '部署全部分词修复；来源元数据恢复Job持续调度。'),
 'SEG02':('已有成功分词，正文v1改v2；暂停消费者。另先选择v1输出，再发布/重算v2，最后装配响应。', '实际所选输出来源与分词不一致则segments=null；同v2正常仍下发；题目主体不因派生失败回滚。', '同步/发布、缓存、输出生成、RPC', '输出来源DDL、元数据cache字段、持续恢复Job。'),
 'SEG03':('旧rev1/MATERIAL任务停在写锁前；新正文rev2/ANSWER_AREA成功后恢复；另用旧空结果试删除。', '锁内重读真实修订号，拒绝整个旧写删；不插旧类型、不删新类型；失败诊断单独提交。', '跨内容位并发、事务、失败收口', '真实MySQL题行锁及双源事务需联调。'),
 'SEG04':("正文含cat2dog、3look up4、1972、low-temperature、it's。", "数字作为分隔，cat/dog/look/up独立；纯数字忽略；hyphen和撇号保留合法词内连接。", '词边界、短语首尾、补录输入', '共享EnglishWords同步改动。'),
 '01':('CSV译文开启引号未闭合，后面跟合法记录；对照引号内合法换行和双引号转义。', 'EOF拒绝畸形文件，不能吞后行后正常写库；合法CSV解析不回归。', '文件解析、整批导入、重导', '无新增基础设施。'),
 '02':('get up带got up|起床混合变体；修正为got up|gotten up后重导。', '非法变体使该短语整行零写；修好后可首次完整入库，避免残缺行占幂等键。', '变体原子接受、幂等重导', '无新增基础设施。'),
 '03':('import开关开启，显式limit=0/-1/null/小数/溢出/字符串，再测省略及合法正数。', '非法值在下载/百度/写库前拒绝；省略才表示全量；正数只处理前N合法候选。', '导入参数、真写范围、dryRun', '保持原XxlJob入口。'),
 '06':('PROD提交后让broker/Redis失效失败，或退出进程；恢复后跑outbox Job，模拟重复投递。', '待办与业务事务同提交；失败不丢，确认后记回执；重放幂等。Boolean只确认PROD提交，不冒称消费可见。', 'PROD事务、MQ、缓存、重放', '两库outbox DDL；englishDictOutboxReplay持续注册。'),
 '10':('对已发布、人工草稿、导入草稿、已删同词重复消费；新词竞争另验。', '先幂等预读，已有词不再调百度；锁内再查封并发；人工接管词不自动发布。', '补录配额、幂等、人工发布闸门', 'auto_publish_eligible迁移先行，历史默认0。'),
 '13':('pos为n./重复40次；再用全部20种合法词性与未知词性。', '合法串规范化后实际落库，规范结果不超96；未知值拒绝；不能只校验后写原串。', '保存、词性回显、列宽', '不扩VARCHAR96，不改枚举业务口径。'),
 '14':('保存take off，搜索 take  OFF 等连续内部空白；再测空条件、部分take与原词。', '搜索与保存共用normalize，归一化后精确匹配；不扩大为模糊或变体搜索。', '单词/短语分页搜索、查重', '无新增基础设施。'),
 '23':('批量发布250词，传limit=0/纯0/仅逗号/负数/非法type；对照150词和2短语。', '所有非法显式参数零发布；省略才全量；合法150/2严格限量。', '批量发布参数、跨库真写', '保留原Job与HTTP契约。'),
 'COND-IPA':('正文含ˈfəʊnəm、and/or、普通[cat]，另构造有明确标记的纯ASCII音标。', '可识别音标整token排除，无f/n/m残片；and/or及普通正文保留；无标记纯ASCII歧义明确保留边界。', '富文本抽取、词边界、补录', '输入若为纯ASCII音标需明确语义标记。'),
 'COND-INDEX':('远端短语版本变更，重装DB失败/Redis版本不可读；对照查询读与落库写。', '读可降级旧表；写侧无法证明新鲜度则不持久化旧块；恢复后重新计算。', '索引、保存/发布/全量写入口', '版本检查配置与重装退避；真实故障注入待验。'),
}
headers = ['改动ID','关联问题/类型','业务改动','改动前行为','修复代码逻辑','完整代码位置','验证前置与操作','预期结果/观察点','已执行证据','待验证项','影响与回归范围','DDL/配置/上线依赖','交付状态']
rows=[]
for index,item in enumerate(coverage['items'],1):
    key=item['id']; old=original.get(key,{}); case=cases[key]
    locations='\n'.join(f"{x['path']}:{x['line']} {x.get('symbol','')}" for x in item['locations'])
    rows.append([f'CHG-{index:03}',key+' / '+('P'+str(old.get('p',2))+'正式问题' if item['kind']=='formal' else '条件边界'),old.get('title',key),old.get('mechanism','见原分析条件风险；不把策略歧义冒充已发生故障。'),item['logic'],locations,case[0],case[1],item['verification'],'JUnit/真实MySQL、Redis、MQ、TTS/OSS、RPC及C端联调未完成；具体探针只证明列明分支。',case[2],case[3],'已给完整代码；业务验收待验证'])

extra=[
 ('REV-R1','题表锁字段契约','旧题表错用delete_time','使用cc_new_question.del_flag，保持新分词表delete_time。','verification/green-sql-contract.txt','SQLite仅验证列/筛选契约，不证明MySQL FOR UPDATE锁。'),
 ('REV-R2','派生失败不回滚题目同步','同步事务内派生写失败污染rollback-only','hook事务内只发事件；提交后独立失信，失败仍投计算；响应来源门保护读侧。','verification/green-segment-wave.txt','题目同步提交、派生表不可用、消费者暂停需业务联调。'),
 ('REV-R3','失败登记在原事务退出后提交','FAILED写后throw导致一起回滚','计算Tx回滚释放题锁后，新Tx重锁核revision/指纹再登记FAILED。','verification/green-segment-wave.txt','真实Spring+内存事务manager不等于真实JTA/MySQL并发。'),
 ('REV-R5','同秒人工接管保护','createTime==updateTime无法证明未编辑','显式auto_publish_eligible：历史0、新补录1、人工保存清0；锁内读资格。','verification/green-eligibility.txt','两库新列与历史迁移；完整人工保存链需IDE和业务验证。'),
 ('SOURCE-01','响应选择后发生更新','仅核新DB正文仍会给旧响应配新序列','生成来源、输出scalar hash、本次选定URL/来源和分词来源绑定；v1/v2不同即省略。','verification/green-response-binding.txt','真实输出生成/资源跨库/缓存失效链尚未联调。'),
 ('SOURCE-02','缓存JSON往返保留身份','nested null丢失可能使同一正常输出hash失配','从同一OutputData生成scalar输出hash并缓存，capture只复制scalar。','verification/green-cache-roundtrip.txt','旧无scalar缓存安全失信；须真实生成和驱逐恢复，不贴新DBhash。'),
 ('SOURCE-03','持续恢复与急停','把一次迁移误当未来发布的永久恢复者','恢复Job持续调度；每库50滚动扫描；执行前和每题重生成前检查总开关/急停。','VERIFICATION.md','正常扫描周期按ceil(N/50)+1个成功调度轮估算；锁跳过/暂停/失败不保证固定墙钟上界。'),
]
for index,(key,title,before,after,evidence,limits) in enumerate(extra,len(rows)+1):
    rows.append([f'CHG-{index:03}',key+' / 复审补充回归',title,before,after,'见FIX-LOGIC.md、REVIEW-FINAL.md及source-manifest.json',
                 title+'：按对应验证脚本/VERIFICATION所列前置执行，并覆盖正常对照。',after,
                 evidence+('（文件已存在）' if (root/evidence).is_file() else '（以最终验证清单为准）'),limits,
                 '来源一致性、事务隔离、旧数据滚动升级','见VERIFICATION.md；持续注册englishSegmentOutputIdentityRecover、englishDictOutboxReplay。','代码与复审结论见最终记录；不等于业务通过'])

meta={'baseline':base['head'],'formal_issues':17,'conditional_boundaries':2,'supplementary_scenarios':7,'matrix_rows':len(rows),'final_review':'规格/代码质量审查通过，详见REVIEW-FINAL.md','business_execution':'未执行真实业务环境验收','headers':headers,'rows':rows}
(root/'CHANGE-MATRIX.json').write_text(json.dumps(meta,ensure_ascii=False,indent=2))
escape=lambda s:str(s).replace('|','\\|').replace('\n','<br>')
md='# 统一修复改动矩阵\n\n26行改动/回归场景：覆盖17正式问题、2条件边界及7项复审补充。代码交付和隔离探针不等于业务环境通过。\n\n'
md+='| '+' | '.join(headers)+' |\n| '+' | '.join(['---']*len(headers))+' |\n'
md+='\n'.join('| '+' | '.join(escape(c)for c in row)+' |'for row in rows)+'\n'
(root/'CHANGE-MATRIX.md').write_text(md)
th=''.join('<th>'+html.escape(h)+'</th>'for h in headers)
trs=[]
for row in rows:
    cells=''.join('<td>'+html.escape(str(c)).replace('\n','<br>')+'</td>'for c in row)
    trs.append('<tr>'+cells+'</tr>')
page='''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>597084 统一修复改动矩阵</title><style>
body{margin:0;padding:24px;font:14px/1.65 system-ui,sans-serif;background:#f3f6fa;color:#182333}h1{font-size:25px}header{background:#142b4a;color:white;padding:22px;border-radius:12px}input{font-size:15px;padding:10px;width:min(600px,90%);margin:16px 0}.scroll{overflow:auto;max-height:78vh;background:white;border:1px solid #d6dfe9}table{border-collapse:separate;border-spacing:0;min-width:3300px;width:100%}th,td{padding:12px;border-right:1px solid #e1e6ec;border-bottom:1px solid #e1e6ec;vertical-align:top;min-width:180px;max-width:330px;overflow-wrap:anywhere}th{position:sticky;top:0;background:#173759;color:white;z-index:3}td:first-child{position:sticky;left:0;background:#eaf1fb;min-width:85px;max-width:85px;font-weight:700;z-index:2}th:first-child{left:0;z-index:4}tr:nth-child(even)td{background:#f6f8fb}tr:nth-child(even)td:first-child{background:#eaf1fb}small{color:#d6e4f5}.note{background:#fff4d8;padding:12px;margin:12px 0}a{color:#1261af}@media print{.scroll{max-height:none;overflow:visible}input{display:none}}
</style><header><h1>英语词典统一修复 · 改动矩阵</h1><p>17项正式问题＋2项条件边界＋7项复审补充，共26行；按测试用例方式追踪改动和验证。</p><small>固定分析基线：0baa9ebe8b7f9ecc46f675acbf1f56ac65378957。尚未执行全项目Maven/JUnit及真实业务联调。</small></header><p><a href="README.md">统一修复说明</a> · <a href="FIX-LOGIC.md">完整修复代码逻辑</a> · <a href="VERIFICATION.md">验证与部署前置</a> · <a href="REVIEW-FINAL.md">最终独立复审</a></p><div class="note">最终规格/代码质量审查通过；业务环境仍待验。相关文件链接需解压统一修复包后在同目录打开。代码已给出、隔离探针GREEN、独立复审通过与业务环境验收通过是不同状态。具体断言与证据逐行列出，不能仅凭本矩阵勾选上线。</div><label>筛选：<input id="filter" placeholder="问题编号、代码、业务场景、证据或依赖"></label><span id="count"></span><div class="scroll"><table><thead><tr>'''+th+'''</tr></thead><tbody>'''+''.join(trs)+'''</tbody></table></div><script>const rows=[...document.querySelectorAll('tbody tr')],input=document.getElementById('filter');function update(){const s=input.value.toLowerCase();let n=0;for(const r of rows){r.hidden=!r.textContent.toLowerCase().includes(s);if(!r.hidden)n++}document.getElementById('count').textContent=n+' / '+rows.length+' 行';}input.addEventListener('input',update);update();</script></html>'''
(root/'CHANGE-MATRIX.html').write_text(page)
print(json.dumps({'rows':len(rows),'formal':17,'conditional':2,'supplementary':7},ensure_ascii=False))
