from pathlib import Path
import json, html, shutil

root=Path(__file__).resolve().parent
source=json.loads((root/'findings.json').read_text())
by_id={x['id']:x for x in source['formal']}

# id, business title, trigger, expected, actual, impact, QA verification, scope
items=[
('AUDIO-N01','系统补齐发音时，把尚未发布的人工修改也发布了',
 '某个词以前发布过，但还没有发音。系统正在生成发音时，运营修改了翻译、上传了音频，只保存，没有点击发布。',
 '学生仍看到上次正式发布的内容；这次人工修改要等运营确认发布后才能生效。',
 '系统处理发音任务完成后，会再次发布整条词条，把刚保存的翻译、名称或变形一起发布。',
 '人工审核与发布控制失效，学生可能提前看到未经确认的内容。这是本轮应优先处理的问题。',
 '先发布一个无发音词，再让发音生成保持处理中；修改翻译并上传音频，仅保存。恢复发音任务后，对照后台草稿、学生可见翻译和发布状态。也要覆盖短语及其变形。',
 '仅限“曾经发布过、后来又保存了未发布修改，并且发音任务仍在处理”的词条；从未发布的词不会因此自动发布。'),
('AUDIO-N02','发音处理长期失败后，任务不会正常结束',
 '发音平台显示已生成，但文件一直无法下载或保存；或者系统一直无法查询生成进度。等待时间已超过设定的超时时间。',
 '超过超时后应结束这次尝试，留下失败原因，并按规则重试或转人工处理。',
 '这两类失败没有经过超时结束处理，系统会一直保留该任务，后续反复处理它。',
 '该词可能长期没有发音；如果这类旧任务占满单次处理名额，后面的任务也会被拖住。',
 '分别模拟“已生成但下载持续失败”“进度查询持续失败”，让等待时间超过30分钟，再运行处理任务。检查任务是否结束、是否记录失败、后面的任务能否继续。短暂失败后恢复成功作为对照。',
 '限定持续失败场景；普通“生成中”超时已有处理，不应笼统描述为所有任务都不超时。'),
('AUDIO-N03','发音每次生成失败，系统仍可能无限重试',
 '平台每次都成功受理请求，但生成结果每次都是失败、取消或超时。',
 '同一条记录达到规定的失败次数后，应停止自动提交，避免反复消耗资源。',
 '系统主要统计“请求没有提交成功”的次数；请求已经受理、后来生成失败的情况没有按同一规则累计。',
 '配置了失败上限，也可能挡不住反复提交，持续消耗生成配额和处理时间。',
 '让提交请求连续成功受理、生成结果连续失败，至少完整跑三次。检查是否停止第四次自动提交；再对照“提交请求本身连续失败”的情况。',
 '提交请求本身连续失败时已有次数限制；缺口在受理后的生成失败、取消与超时。'),
('AUDIO-N04','前面几条词生成失败，后面的正常词也可能一直轮不到',
 '排在最前面的词连续提交失败，已经达到重试上限；后面还有正常词等待生成发音。',
 '跳过暂时不能处理的词，继续处理后面的词，不能让少量失败记录挡住整个队列。',
 '每轮仍先取最前面的同一批词，再发现它们不能重试；没有继续取后面的记录。',
 '平台恢复后，后面的健康词也可能长期没有发音。按当前失败记录保留规则，影响可能持续数日。',
 '把每次处理数量设为1，准备两个待生成词。让第一条达到失败上限后恢复平台，重复运行任务，检查第二条能否提交。',
 '需要前面的失败记录占满一次处理名额，不是任意一条失败都会阻塞全部词。'),
('AUDIO-N05','发音补齐了，词条仍显示“有缺失”，查询也可能还是旧内容',
 '一个词的音标、翻译等内容已经齐全，只缺发音；此前已查询过它，随后系统成功补齐发音。',
 '后台完整性应变为“完整”，学生查询应能看到新发音，并得到正确的完整性标识。',
 '系统只补了发音，没有同步重新计算完整性，也没有完整处理后台查询中保留的旧内容。',
 '完整性筛选、发布确认和下游降级展示可能失真；实际有发音，查询仍可能暂时显示没有。',
 '先查询一次只有发音缺失的词，再让系统补齐发音，分别检查后台列表、详情和学生查询。另准备仍缺音标或翻译的词，确认它们仍应显示有缺失。',
 '只有发音确为最后一个缺失字段时，才应断言完整性变为“完整”。'),
('SEG01','题目刚同步就发布，学生端可能一直没有查词功能',
 '首次同步一道英语题后立即发布，此时后台还没有完成这道题的划词处理，或首次处理失败。',
 '题目发布不应依赖操作人员重复发布；后台计算完成后，学生端应能获得对应的可查词内容。',
 '首次发布可能没有为学生侧建立后续可处理的记录；后续单题处理和每日重算又都会跳过这种缺失记录。',
 '即使后台后来算好了，学生端仍可能一直没有可点击词，直到人工再次发布题目。',
 '暂停后台划词处理，首次同步后立即发布；再恢复处理，并运行一次每日重算。检查学生查询是否最终出现划词；再次发布能恢复的情况也要记录。',
 '主要是首次发布与后台计算先后顺序的竞争；计算已成功后再发布的正常对照通常能工作。'),
('SEG02','题目正文已经变了，下发的可点击词仍可能来自旧正文',
 '一道已有查词功能的题修改了英文正文，但后台重新划词延迟、失败，或查询过程中刚好发生版本更新。',
 '正文与可点击词必须对应同一个版本；无法确认一致时，应暂时不提供划词，避免标错。',
 '旧划词结果在后台重新计算前仍可能被当作有效内容下发；查询先取旧正文、后取新划词时，也存在组合错配的风险。',
 '下游可能收到“新正文配旧词表”或相反组合，出现漏标、错位或词无法定位。具体客户端表现尚未实测。',
 '修改正文后延迟后台处理，检查返回正文与划词是否对应；另固定先读取旧正文，再完成新正文及划词更新，最后完成同一次查询，检查是否混用了两个版本。',
 '不能只检查后台是否最终算成功，还要检查每次实际返回的正文与词序列是否一致。'),
('SEG03','旧计算晚完成，可能删掉刚算好的新版结果',
 '同一道题从旧内容格式切换到新格式；旧版计算尚未完成，新版计算已经成功，随后旧计算才继续。',
 '较旧的计算应被整体拒绝，不能覆盖或删除较新的有效结果。',
 '现有保护主要约束同一类内容；清理旧内容时没有完整保护整道题，旧计算可能删除新版结果。',
 '学生侧可能重新拿到旧词序列，或失去本来已可用的查词结果，需要再次处理才能恢复。',
 '先启动旧版计算并暂停；修改内容格式，完成新版计算；再放行旧计算。检查新版结果是否保留。再补一个“旧版内容为空”的删除场景。',
 '依赖内容格式/处理范围发生变化；稳定的同一类内容已有部分旧版本拦截，不能说所有并发都会出错。'),
('SEG04','数字夹在英文中时，正常单词没有被正确分开',
 '题目出现cat2dog、3look up4等数字与英文相邻的内容。',
 '按需求，数字作为分隔；例如cat2dog应识别cat、dog，纯数字不参与查词。',
 '系统仍可能把cat2dog、3look、up4当作完整词处理。',
 '正常词和短语无法匹配；还可能把带数字的无意义内容当成新词补入词库。',
 "验证cat2dog、3look up4、纯数字1972，并对照low-temperature、it's，确认数字切开而合法连字符、撇号不被误切。",
 '纯数字已能被忽略，问题在英文和数字混在一起的情况。'),
('01','导入文件少一个结束引号，后面的词条会被吞进上一条译文',
 '短语导入文件中，一条译文以引号开始，但漏了结束引号，后面还有其他合法词条。',
 '应识别文件格式错误并拒绝这次错误数据，不能把后面的词条作为普通译文导入。',
 '后续多行可能被并入上一条译文；系统仍把它当成合法词条，后面的独立词条丢失。',
 '产生错误释义和漏导数据；修好文件重新导入时，已存在词条又会被跳过，错误不能靠重导自动纠正。',
 '准备look up的译文缺结束引号、下一行是get up的文件，检查解析与导入结果；再验证合法的多行译文和引号转义不受影响。',
 '属于已检测不足的文件格式边界，不是正常的译文换行功能本身有问题。'),
('02','短语的一部分变形有错，系统仍先导入半条数据，修好后却补不进去',
 '同一短语同时带合法和非法变形，例如get up带got up和一段非法内容。',
 '对这类格式错误，应让整条短语保持未导入；修正文件后可以完整重导。',
 '系统只丢弃非法变形，却导入主短语及其余变形；重导发现主短语已经存在，就跳过整条。',
 '缺失的变形无法通过修文件重导补齐，题目遇到该变形时可能无法按短语查词。',
 '先导入含一合法、一非法变形的短语，再修成两个合法变形重导；检查首次是否已经占用词条、第二次能否补全。',
 '应区分文件格式错误与三方内容暂缺：这里讨论的是已发现非法变形后仍部分写入。'),
('03','试导入条数填错，可能变成整份文件真实导入',
 '导入功能已开启，原本想小批试跑，却把处理条数填为0或负数，且没有启用仅预览模式。',
 '无效条数应在开始处理前被拒绝；只有明确不限制条数时，才允许处理全量。',
 '0或负数可能被当作“不限制条数”，整份文件继续查询词典并新增词条。',
 '小批验证被扩大为全量写入，增加人工清理、外部调用和后续审核工作量。',
 '用明显超过试跑规模的文件，分别填0、负数和合法正数；检查无效值是否零写入、正数是否严格限制，预览模式是否完全不新增。',
 '风险成立的前提是导入开关已开且执行真实导入；省略条数默认全量本身是当前约定。'),
('06','发布提示成功，但查询仍可能是旧内容，重跑批量发布也不会补处理',
 '新内容已经写入学生侧，但让查询更新所需的后续处理发生失败。',
 '应保留可重试的处理记录，恢复后自动追平；不能仅提示成功后就无人继续处理。',
 '错误主要写日志，词条已经显示已发布；再次运行批量发布时，这条词不再属于待发布记录，因此不会重新处理。',
 '运营以为已经生效，学生查询却可能继续看到旧释义、旧音标或旧发音，直到其他更新或人工介入。',
 '先查询并记录旧内容；发布新内容时模拟后续更新失败；检查发布状态、学生查询与恢复记录，再重跑批量发布，确认是否真的会处理这条词。',
 '这是异常恢复缺口，不是把所有短暂的正常生效延迟都算作发布失败。'),
('10','词库已经有这个词，重复补录仍会再次查询第三方词典',
 '多道题遇到同一个词，或后台重复处理同一条补录消息；词库中其实已有该词。',
 '先判断是否已经存在；已存在且无须补充时，不再重复请求第三方词典，也不替人工发布草稿。',
 '系统仍可能先查询第三方词典，再发现本地已经存在而停止新增。',
 '词条不一定重复，但会浪费查询配额和处理时间；大量重复处理时会放大外部服务压力。',
 '分别准备已发布词、人工草稿、导入草稿和已删除词，重复触发补录并统计第三方查询次数；同时检查人工草稿没有被系统发布。',
 '本轮音频已改为后续统一生成，不再沿旧报告声称每次重复补录都会重复生成和上传发音。'),
('13','词性内容被校验为合法，保存时却仍可能超长失败',
 '测试直接提交包含重复词性或较多空格的较长词性内容；系统能把它理解为合法词性组合。',
 '应把整理后的规范词性实际保存；如果整理后仍超过限制，应明确拒绝。',
 '校验时使用整理后的结果，保存时却可能仍写原始长内容，超过可保存长度。',
 '原本已通过业务校验的数据，在保存阶段报错或被截断。是否出现截断取决于实际数据库设置，当前未做实库断言。',
 '直接提交重复词性组成的长串，检查最终保存值是否已去重、去多余空格；再测全部合法词性与未知词性。页面是否允许此输入要另验。',
 '问题不是允许复合词性本身，而是“校验用的内容”和“实际保存的内容”不一致。'),
('14','同一个短语能保存、能判重，但用原输入搜索却搜不到',
 '保存take off后，搜索时输入take  off，两个词之间多一个空格。',
 '保存、查重、搜索应遵循同一套空白处理规则，合理的空格差异不应导致搜不到。',
 '保存时会合并连续空格，搜索时没有完整做相同处理，因此精确查询无法命中。',
 '运营搜不到已有词，尝试新增又被提示重复，影响查找和维护。',
 '对照一个空格、多个空格、首尾空格和空条件；确认完整短语能命中，部分输入take仍不应变成模糊搜索。',
 '此项用内部空格差异即可证明，不依赖未经查询的大小写匹配设置。'),
('23','批量发布条数填0或无效内容，可能把全部待发布词直接发布',
 '在批量发布任务中显式填写条数为0，或者只填逗号等无效内容。',
 '应拒绝无效设置，且不能发布任何词；合法条数应严格限量。',
 '这些输入可能被当作全量发布。原隔离探针中，填0实际处理了全部250条。',
 '原本想小范围验证，却可能把尚未审核完成的待发布词全部送到学生侧，属于真实发布范围扩大。',
 '准备250条待发布词，先填0、纯0、逗号，检查是否零发布；再填150或只发2条短语，确认数量和类型都受限制。',
 '没有填写数量时默认全量是当前约定；问题在“显式填错”也被当作全量。'),
]

def md_card(i,row):
 key,title,trigger,expected,actual,impact,test,scope=row
 f=by_id[key]
 return f'''## 问题{i:02d}｜{title}

**处理优先级：P{f['p']}。**

**什么情况下发生：** {trigger}

**正常应该怎样：** {expected}

**目前有什么问题：** {actual}

**影响是什么：** {impact}

**测试怎么确认：** {test}

**影响边界：** {scope}

'''

intro='''# 英语词库与题目查词：QA分析报告

**结论：分析时的代码版本仍有需要修复和验证的问题。最优先处理的是：系统补齐发音时，可能把人工尚未发布的修改一并发布。**

本轮记录17项明确问题（P1一项、P2十六项），另有2项需要明确规则的条件风险。P1表示本轮优先处理；P2表示影响局部功能或需要特定条件触发，仍需安排修复与回归。

本报告依据2026年11月4日的需求与当时固定代码版本。后来已单独交付修复代码和改动矩阵，但没有据此认定测试环境已经部署、业务已经验收，下面描述的是本轮分析发现的问题。

## 先理解本次改动

| 业务操作 | 改动前 | 改动后 | 测试要重点看什么 |
|---|---|---|---|
| 题目同步 | 同步提交后直接处理可查词内容 | 同步先完成，后台稍后处理 | 处理延迟或失败时，正文和可点击词不能配错 |
| 题目首次发布 | 带上已有处理结果 | 先准备后续处理记录，再由后台计算 | 先发布、后算完能否自行恢复，不能要求人工重复发布 |
| 新词发音 | 建词时等待生成 | 先建词，之后统一生成和补齐 | 等待期间的人工编辑、失败次数、超时和后续更新 |
| 词条发布 | 人工发布后学生可见 | 发音补齐后，部分词还会由系统再次发布 | 系统只能补已确认的内容，不能带出人工草稿 |
| 查询短语 | 按一次命中的原形或变形取详情 | 取短语及其全部变形，再选择对应内容 | 对应变形的音标、发音和显示顺序不能错 |
| 短语维护 | 其他服务可能较晚拿到新词库 | 增加了更快通知和检查机制 | 暂时拿不到新词库时，不能把旧规则算出的结果当成新结果保存 |

## 本次验证能证明什么

已重新核对需求正文、评论和代码，并运行了部分独立复现程序。它们能够证明所列处理分支存在问题，但没有执行真实业务数据库、消息处理、发音平台或客户端全流程，因此不写“线上已发生”或“业务验收通过”。

下面每项都按“发生条件—预期—实际—影响—验证方法”说明。研发定位与原始证据在报告末尾，阅读正文不需要先理解代码。

'''
md=intro+''.join(md_card(i,r)for i,r in enumerate(items,1))
conditional='''## 还需要确认的两项处理规则

### 条件风险一：音标中的部分字母可能被当作正文单词

需求要求音标不参与查词。现有处理可能只去掉音标中的特殊字符，留下f、n、m等普通字母，再把这些字母当作词处理。

测试应准备包含音标、普通英文和and/or等内容的混合正文，确认音标整体不生成假词，同时不误删正常英文。没有任何标记的纯英文形式音标，与普通英文可能无法区分，需要先约定数据如何标记，不能靠随意删除正文解决。

这项仍有输入规则和处理策略需要确认，未与17项明确问题混为同一类。

### 条件风险二：词库暂时不可用时，到底允许系统继续到哪一步

系统暂时拿不到最新词库时，可以选择让现有查询继续提供旧内容，保证部分功能可用。但如果重新处理题目时也使用旧词库，并把结果保存为成功，就可能把短暂故障变成长期的旧划词结果。

测试应区分“暂时查询到旧内容”和“把旧规则算出的结果保存下来”。产品和研发需要明确：拿不到新词库时，是暂停保存新划词结果，还是接受旧结果及其恢复时限。目前材料的可用性取舍与正确性承诺需要统一。

## 建议的测试顺序

1. 先测发布控制：人工修改只保存时，学生侧绝不能提前看到新内容。
2. 再测处理先后顺序：首次同步后立即发布、修改正文后处理延迟、旧计算晚于新计算完成。
3. 集中测发音失败：下载失败、查询失败、生成失败、超时、达到重试上限，以及失败词是否挡住后续词。
4. 核对后台与学生侧的最终结果：发音、完整性、翻译、可点击词与正文必须对应。
5. 最后覆盖导入和维护边界：坏文件、部分非法变形、无效条数、词性长内容、空格搜索、数字及音标处理。

## 已有修复与尚未验收的区别

此前的作答区取错字段、导入来源标记、补录与人工编辑相互等待等问题，已在本轮代码中看到修正，不重复列成当前问题。

本报告发现的问题随后已有独立修复代码交付；这只表示有代码可供审查。是否实际解决，仍需在部署对应代码和配置后，按上述步骤检查后台、学生侧数据与异常恢复，不能仅凭代码审查或少量复现程序通过销账。
'''
md+=conditional
(root/'QA-问题说明.md').write_text(md)

H=html.escape
style='''body{margin:0;background:#f4f7fb;color:#1e293b;font:16px/1.8 system-ui,-apple-system,"PingFang SC",sans-serif}main{max-width:1080px;margin:auto;padding:28px 20px}header{padding:28px;background:#153456;color:white;border-radius:14px}h1{font-size:28px;line-height:1.5;margin:0 0 10px}h2{font-size:23px;margin:36px 0 16px}h3{font-size:20px;line-height:1.6;margin:0 0 14px}.card{background:white;border:1px solid #d9e1ed;border-radius:12px;padding:24px;margin:18px 0}.p1{border-left:6px solid #c45d19}.p2{border-left:6px solid #3471a8}.priority{font-size:14px;font-weight:600;color:#87450c}.p2 .priority{color:#225b8a}p{margin:12px 0}b{color:#163b64}.notice{padding:16px 20px;background:#fff4d9;border-radius:8px;margin:20px 0}.muted{color:#607087;font-size:14px}.scroll{overflow:auto}table{border-collapse:collapse;width:100%;min-width:650px}th,td{text-align:left;vertical-align:top;padding:12px;border:1px solid #dce3ed}th{background:#edf2f9}nav{display:flex;gap:12px;flex-wrap:wrap;margin:20px 0}a{color:#145da5}input{width:min(650px,90%);padding:12px;font:inherit;border:1px solid #b6c6dc;border-radius:8px}details{background:#eef2f8;padding:14px;border-radius:8px;margin:12px 0}code{overflow-wrap:anywhere}li{margin:10px 0}@media(max-width:600px){main{padding:16px 12px}.card{padding:18px}h1{font-size:24px}body{font-size:16px}}@media print{nav,input{display:none}.card{break-inside:avoid}body{background:white}header{background:white;color:#1e293b}main{max-width:none;padding:0}}'''
cards=[]
appendix=[]
for index,row in enumerate(items,1):
 key,title,trigger,expected,actual,impact,test,scope=row;f=by_id[key]
 cards.append(f'''<article class="card p{f['p']}" id="issue-{index:02d}" data-issue><h3>问题{index:02d}｜{H(title)}</h3><span class="priority">P{f['p']} · {'本轮优先处理' if f['p']==1 else '需修复并安排回归'}</span><p><b>什么情况下发生：</b>{H(trigger)}</p><p><b>正常应该怎样：</b>{H(expected)}</p><p><b>目前有什么问题：</b>{H(actual)}</p><p><b>影响是什么：</b>{H(impact)}</p><p><b>测试怎么确认：</b>{H(test)}</p><p class="muted"><b>影响边界：</b>{H(scope)}</p></article>''')
 url='https://git.xjjj.co/cc/backend/course-center/-/blob/'+source['head']+'/'+f['file']+'#L'+str(f['start'])
 appendix.append(f'<li>问题{index:02d}（原编号{H(key)}）：<a href="{H(url)}">代码依据</a>；<a href="{H(f["evidence"])}">原始核验记录</a></li>')

# Use the existing local Markdown renderer solely to preserve the QA introduction,
# tables and rules; issue cards are generated explicitly above.
(root/'qa-intro.md').write_text(intro.replace('# 英语词库与题目查词：QA分析报告\n','',1))
(root/'qa-conditions.md').write_text(conditional)
(root/'qa-cards.html').write_text(''.join(cards))
(root/'qa-appendix.html').write_text('<details id="technical"><summary>研发定位与原始证据（按需展开）</summary><p>以下保留技术定位，供研发核对，不作为阅读正文的前提。</p><ul>'+''.join(appendix)+'</ul><p><a href="technical-report-original.html">查看改写前的技术版报告</a></p></details>')
(root/'qa-style.css').write_text(style)
print(json.dumps({'issues':len(items),'conditional_rules':2,'qa_markdown':str(root/'QA-问题说明.md')},ensure_ascii=False))
