Răsfoiți Sursa

docs(release): verify reading print answer alignment and production export

彭峰 3 ore în urmă
părinte
comite
b1af307748

+ 58 - 0
docs/qa/reading-answer-distribution-release-2026-11-15.json

@@ -0,0 +1,58 @@
+{
+  "date": "2026-11-15",
+  "timezone": "Asia/Shanghai",
+  "releaseId": "mobile-reading-answers-e02506e38cb6-20261115T102519376Z",
+  "sourceCommit": "e02506e38cb67506653fa2e1dd4cf51f67bbcb1b",
+  "mobileTestsPassed": 417,
+  "readingTestsPassed": 51,
+  "liveResourcesVerified": 105,
+  "routesVerified": [
+    "words/reading",
+    "words/reading/pdf/readonly-check",
+    "pages/home/index",
+    "pages/his/his",
+    "pages/companion/index"
+  ],
+  "cdn": {
+    "id": "8200599242",
+    "status": "task_done",
+    "processing": 0,
+    "succeed": 1,
+    "failed": 0,
+    "total": 1
+  },
+  "samples": {
+    "articles": 21,
+    "questions": 105,
+    "unchangedSourceQuestions": 105
+  },
+  "browser": {
+    "candidate": true,
+    "productionAssets": true,
+    "productionBusinessRequestsIntercepted": true,
+    "isolatedSubmissionsPerRun": 1,
+    "errorsPerRun": 0,
+    "retryRefreshPdfReturn": true,
+    "pdfPagesVisuallyCheckedPerRun": 4,
+    "wechatViewport": true,
+    "physicalDevice": false
+  },
+  "model": {
+    "modelCalls": 1,
+    "questions": 5,
+    "answers": [
+      "D",
+      "A",
+      "C",
+      "D",
+      "B"
+    ],
+    "productionBusinessWrites": 0,
+    "errors": []
+  },
+  "prompt": {
+    "updatedAt": "2026-11-15T10:27:04.035Z",
+    "sha256": "261c2d72e1ea338de77345ee1e00f388a95397d8fc02e0875ef49cbc9759e760",
+    "singleRecord": true
+  }
+}

+ 45 - 0
docs/qa/reading-answer-distribution-release-2026-11-15.md

@@ -0,0 +1,45 @@
+# 阅读理解打印答案分布与解析一致性发布 — 2026-11-15
+
+用户已确认问题来自打印/PDF,并授权按方案修复、测试后发布。本次已发布至 https://h5.xiaoshu.pro/ 。
+
+## 发布结果
+
+- H5 版本:`mobile-reading-answers-e02506e38cb6-20261115T102519376Z`,源码提交 `e02506e38cb67506653fa2e1dd4cf51f67bbcb1b`。
+- 同步更新唯一一条 `english-reading-question-json-tpl` 云端提示词;修改前核对完整旧文本及 `updatedAt`,保留条件回滚脚本。未更新后端网关、管理后台或原生安装包。
+- 发布基线是正在运行的报告字体版本 `mobile-report-fonts-b8fe8b13111f-20261115T100518307Z`。使用其已验证源码快照,仅叠加阅读文件;保留既有字体等线上改动,未重新带入其他性能候选。
+- 源站、公网版本清单一致,105 个资源逐文件 SHA-256 验证通过;5 条不带版本参数的深层路由回读通过。
+- CDN 任务 `8200599242`:`task_done`,成功 1,失败 0。
+
+## 修复行为
+
+1. 一套五题通过稳定种子安排题序及选项,A/B/C/D 各至少一次,另一次随机分配。移动完整选项对象,正确内容及 `check` 保持关联。
+2. 在线答题、打印题目和打印答案共用同一版顺序。首次打开、刷新、打印返回保持一致;“再做一次”建立新版本。
+3. 解析中的明确选项字母同步映射,题目级和选项级解析一起处理;避免连续替换导致 A→B→C 串改。保留普通英文冠词、“Plan A”“维生素A”等内容。
+4. 对不能可靠重排的位置依赖题或解析,明确提示核对而不输出错配试卷。本次全部 21 套真实样本可正常呈现。
+5. 重做版本记录前一作答 ID,刷新/PDF 返回不复活旧成绩,新提交成功后仍能恢复新作答。
+6. 生成示例不再每题固定首项为正确;新增校验和提示词要求,解析用内容与原文依据说明,避免引用字母或位置。
+
+## 验证
+
+- 初始阅读基线 38 项通过。新增测试先复现原问题:打印分布失败、映射/不变量失败、在线版本/生成偏置失败。
+- 最终阅读专项 **51 项通过**;实际发布源码快照的移动端全量 **417 项通过**;生产构建通过。构建仍包含项目原有 CommonJS 警告,测试音频夹具仍有既有 404 提示,未伪称日志完全无告警。
+- 一次独立审查发现四项问题,均建立失败回归后修复:旧解析的冒号/英文引用、普通字母误改、重做恢复旧答案、生成解析中的位置依赖。
+- 只读取得今天 21 篇文章和 105 道题,数据级验证全部题组分布为 1/1/1/2,正确内容及四个选项集合完全保留,输入没有变更。
+- 浏览器分别在本地发布候选和正式站资源上运行完整检查:逐套比较 **21 套、105 道题**的在线顺序、PDF 题目顺序、PDF 答案区顺序及正确内容,全部一致。
+- 两次浏览器运行均用隔离接口接入上述真实题目样本,不把模拟提交写入生产。每次完成一套模拟 100 分提交,检查提交后的在线解析与打印解析一致;重做、刷新、PDF 返回保持新版本且不恢复旧选择;JavaScript 错误均为 0。
+- 调用真实生成模型一次,以合成花园短文通过实际候选前端发出的提示词生成五题;无生产业务写入。生成与校验通过,显示答案 `D/A/C/D/B`,逐题核对解析与原文、正确内容一致。
+- 本地及正式资源都通过应用实际导出按钮生成四页 PDF,并渲染逐页目视检查:题干、选项、正确标记及解析对应,无文字截断或错位。验收样本答案为 `B/B/A/C/D`。
+- 390px 移动视口无横向溢出;微信 UA 下导出提示正常。没有实体手机或物理打印机验收,不能把浏览器模拟等同实机。
+- 发布后再次只读回查原 105 道题的题干、选项、解析、题号、关联文章,全部与发布前一致;未改写学生历史题目和成绩。
+
+## 证据与回滚
+
+聚合结果:`docs/qa/reading-answer-distribution-release-2026-11-15.json`。
+
+私有证据目录:`/Users/mac/.local/state/xiaoshu/qa/2026-11-15-reading-answer-release/`。包含源码范围核对、生产基线备份、构建/测试日志、题目映射审计、真实模型合成样本、浏览器检查、导出 PDF 与逐页 PNG、原站/公网文件校验、CDN 回执、生产数据回读。
+
+- `rollback-web.py` 先核对当前线上仍属于本次入口/清单,再恢复之前入口并刷新 CDN;保留哈希资源。
+- `patch-prompt.mjs rollback` 只在模板仍等于本次候选内容时还原旧文本,遇到其他修改立即停止。
+- 不需要回滚学生业务数据。
+
+已下载或已打印的旧 PDF 不会自动改变。用户需刷新阅读页面,重新导出/打印。此次验证的是选项、答案及解析在重新排列后的对应关系,不等于逐题完成所有历史 AI 内容的教学审校。

+ 36 - 26
docs/superpowers/plans/2026-11-15-reading-answer-distribution.md

@@ -1,6 +1,6 @@
 # 阅读理解答案位置异常:排查与解决方案
 
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
 
 **Goal:** 消除阅读试卷正确答案集中在 A 的问题,并确保在线答题、打印试卷、答案解析使用一致的选项对应关系。
 
@@ -8,7 +8,7 @@
 
 **Tech Stack:** Angular 18、TypeScript、Jasmine/Karma、现有 Parse 阅读接口。
 
-**Spec:** 本文“调查结论”“目标与约束”是本方案的依据。当前用户请求为先查原因、再制定方案;本文任务均尚未实施。
+**Spec:** 本文“调查结论”“目标与约束”是本方案的依据。初次请求为先查原因、再制定方案;用户随后确认问题来自打印/PDF,并授权按方案修复、测试后发布。2026-11-15 已完成发布,验收见 ../../qa/reading-answer-distribution-release-2026-11-15.md。
 
 ## 调查结论
 
@@ -55,7 +55,7 @@
 - 已有题目无需重生成;历史作答仍按题目 ID 与所选内容恢复,保留成绩和文章。已下载或已打印的旧 PDF 无法被远程更新,需重新导出。
 - 原始选项顺序保留用于解释旧解析的字母;正常英文单词、`Plan A`、文章中人名首字母不被误改。
 - 不完整题组继续使用现有保护,不静默补题后覆盖已完成记录。
-- 本次只产出调查和方案;实施阶段再运行以下开发与发布步骤。
+- 2026-11-15 已获得实施及测试后发布授权;以下记录执行结果。
 
 ## Review Focus
 
@@ -75,11 +75,11 @@
 - `presentReadingQuestions(questions: ReadingQuestion[], seed: string): ReadingQuestion[]`:复制输入,校验完整题组,根据种子产生题序和近均衡位置,移动完整选项对象。
 - `remapReadingExplanation(text: string, labels: Readonly<Record<string, string>>): string`:仅映射无歧义选项引用;从原数组索引构造旧字母→新字母映射,单次替换,防止 A→B→C 的级联误替换。
 
-- [ ] 写失败测试:五题原始全 A;同一种子结果稳定;不同种子可产生不同结果;每个字母出现 1–2 次;正确内容、选项解析绑定和原始输入保持不变。
-- [ ] 写失败测试:`A项/B项/选项A/option A` 同步映射;`Plan A` 与普通单词保留;题目和各选项的解析都处理。
-- [ ] 写失败测试:不完整、重复内容、多正确项拒绝进入新题组编排;检测位置依赖选项,不直接重排导致含义变化。
-- [ ] 实现共享函数。对不能可靠转换的旧解析或位置依赖题逐题列入复核,不能用宽泛正则改写所有 A/B/C/D,也不能静默删除解释。
-- [ ] 运行专项测试并检查上述不变量后提交本任务文件。
+- [x] 写失败测试:五题原始全 A;同一种子结果稳定;不同种子可产生不同结果;每个字母出现 1–2 次;正确内容、选项解析绑定和原始输入保持不变。
+- [x] 写失败测试:`A项/B项/选项A/option A` 同步映射;`Plan A` 与普通单词保留;题目和各选项的解析都处理。
+- [x] 写失败测试:不完整、重复内容、多正确项拒绝进入新题组编排;检测位置依赖选项,不直接重排导致含义变化。
+- [x] 实现共享函数。对不能可靠转换的旧解析或位置依赖题逐题列入复核,不能用宽泛正则改写所有 A/B/C/D,也不能静默删除解释。
+- [x] 运行专项测试并检查上述不变量后提交本任务文件。
 
 ### 2. 在线练习与 PDF 共用呈现版本
 
@@ -87,11 +87,11 @@
 
 **Interfaces:** 路由增加非敏感 `presentationVersion`(默认 `v1`);共享种子由文章 ID 与该版本组成。使用任务 1 的 `presentReadingQuestions()`,不再在两个组件内各自安排选项。
 
-- [ ] 写失败测试:同一文章同一版本的在线页、PDF 题目区、PDF 答案区逐题逐选项相同;直接进入未带版本的 PDF 使用默认版本。
-- [ ] 写失败测试:“再做一次”建立新版本,写入当前路由并在打印/返回时携带;刷新不改变当前顺序;切换文章不沿用上一文章答题内容。
-- [ ] 写失败测试:历史 `answer` 按题目 ID 和内容恢复,计算成绩与原结果一致;缺题保护与保存失败重试保持正确。
-- [ ] 接入共享呈现函数和版本参数,继续按内容保存答案,不修改历史 `SurveyLog` 数据。
-- [ ] 专项测试通过后提交;使用真实题目脱敏夹具在浏览器核对在线与 PDF,覆盖至少一组全 A 和一组含字母解析的旧题。
+- [x] 写失败测试:同一文章同一版本的在线页、PDF 题目区、PDF 答案区逐题逐选项相同;直接进入未带版本的 PDF 使用默认版本。
+- [x] 写失败测试:“再做一次”建立新版本,写入当前路由并在打印/返回时携带;刷新不改变当前顺序;切换文章不沿用上一文章答题内容。
+- [x] 写失败测试:历史 `answer` 按题目 ID 和内容恢复,计算成绩与原结果一致;缺题保护与保存失败重试保持正确。
+- [x] 接入共享呈现函数和版本参数,继续按内容保存答案,不修改历史 `SurveyLog` 数据。
+- [x] 专项测试通过后提交;使用真实题目脱敏夹具在浏览器核对在线与 PDF,覆盖至少一组全 A 和一组含字母解析的旧题。
 
 ### 3. 消除生成示例偏置并加强校验
 
@@ -99,22 +99,32 @@
 
 **Interfaces:** 保留现有题目 JSON 字段与接口;新生成题的题干、选项及解释以内容指代答案,不引用会随重排改变的选项字母和位置。
 
-- [ ] 写失败测试:格式示例不再每题固定首项 `true`;云端模板存在和缺失两条路径都附加“解释不依赖选项字母/位置”的要求。
-- [ ] 写失败测试:仅补齐缺失题目、生成重试、保存失败重试不会修改已有题目或破坏五题完整性。
-- [ ] 修改示例使用不同正确位置,校验生成结果里的位置引用。格式示例只能降低偏置,最终呈现分布由任务 1 保证。
-- [ ] 同步调整内置提示词,并准备云端该条模板的精确补丁和回滚副本;发布前核对模板 `updatedAt` 与旧内容,避免覆盖他人更新。
-- [ ] 专项测试通过后提交,保存脱敏生成样本核对内容正确性及解析独立性。
+- [x] 写失败测试:格式示例不再每题固定首项 `true`;云端模板存在和缺失两条路径都附加“解释不依赖选项字母/位置”的要求。
+- [x] 写失败测试:仅补齐缺失题目、生成重试、保存失败重试不会修改已有题目或破坏五题完整性。
+- [x] 修改示例使用不同正确位置,校验生成结果里的位置引用。格式示例只能降低偏置,最终呈现分布由任务 1 保证。
+- [x] 同步调整内置提示词,并准备云端该条模板的精确补丁和回滚副本;发布前核对模板 `updatedAt` 与旧内容,避免覆盖他人更新。
+- [x] 专项测试通过后提交,保存脱敏生成样本核对内容正确性及解析独立性。
 
 ### 4. 验收与发布
 
-- [ ] 专项测试:`npm run test:mobile:ci -- --include='projects/xiaoshu-mobile/src/app/features/english-reading/**/*.spec.ts'`,全部通过。
-- [ ] 全量移动端回归:`npm run test:mobile:ci`;构建:`npm run build:mobile`,均通过。
-- [ ] 把今天 21 套题作为私有只读样本验证:每套呈现后无全 A,四个位置分布符合约束,正确内容与原始 `check` 一致;含字母解析及位置依赖项逐题复核。
-- [ ] 浏览器验收在线选择、提交、历史恢复、再做、刷新、预览和 PDF 导出;提交测试使用专用隔离记录,不冒用真实学生成绩。PDF 逐页检查题目、答案和解析,完成至少一次实体手机/微信环境确认再报告相应覆盖。
-- [ ] 以发布时实际线上版本为基线,仅发布阅读改动,不能重新带入此前已回滚的性能候选。备份入口、资源与该条模板后进行发布和 CDN 版本核验。
-- [ ] 上线后只读观察新生成/呈现题组与错误日志;区分“原始生成位置统计”和“实际呈现位置统计”,不能用前者误判新展示修复失败。
-- [ ] 若需回滚,恢复本次前端入口及此次模板补丁;不回滚学生业务数据。先核对线上仍是本次发布,避免覆盖后续发布。
+- [x] 专项测试:`npm run test:mobile:ci -- --include='projects/xiaoshu-mobile/src/app/features/english-reading/**/*.spec.ts'`,全部通过。
+- [x] 全量移动端回归:`npm run test:mobile:ci`;构建:`npm run build:mobile`,均通过。
+- [x] 把今天 21 套题作为私有只读样本验证:每套呈现后无全 A,四个位置分布符合约束,正确内容与原始 `check` 一致;含字母解析及位置依赖项逐题复核。
+- [x] 浏览器验收在线选择、提交、历史恢复、再做、刷新、预览和 PDF 导出;提交测试使用专用隔离记录,不冒用真实学生成绩。PDF 逐页检查题目、答案和解析,实际完成 390px 移动视口及微信 UA 提示验证;实体手机未连接,不计入已完成覆盖。
+- [x] 以发布时实际线上版本为基线,仅发布阅读改动,不能重新带入此前已回滚的性能候选。备份入口、资源与该条模板后进行发布和 CDN 版本核验。
+- [x] 上线后只读观察新生成/呈现题组与错误日志;区分“原始生成位置统计”和“实际呈现位置统计”,不能用前者误判新展示修复失败。
+- [x] 若需回滚,恢复本次前端入口及此次模板补丁;不回滚学生业务数据。先核对线上仍是本次发布,避免覆盖后续发布。
 
 ## 完成判据
 
-相同版本的在线页、PDF 题目和答案逐项一致;五题正确位置符合约束;重排前后正确内容、历史选中内容和成绩一致;所有解析字母指向当前展示选项;旧题得到兼容;专项、全量和浏览器验收均有证据。当前仅调查与方案完成,以上修复与验收尚未执行。
+相同版本的在线页、PDF 题目和答案逐项一致;五题正确位置符合约束;重排前后正确内容、历史选中内容和成绩一致;所有解析字母指向当前展示选项;旧题得到兼容;专项、全量和浏览器验收均有证据。实现、自动测试、浏览器/PDF 验收及 H5 发布均已完成。实体手机和物理打印机未实测;其他边界与证据见发布报告。
+
+
+## 执行补充与审查裁定
+
+- 重做版本另携带 `retryAfterLogId`,只跳过重做之前的那条成绩记录,已保存的新作答仍可恢复;避免依赖客户端与服务器时钟一致。
+- 为防止误改“维生素A”“Plan A”等正文,只映射有明确语境的字母引用。对已识别但不能可靠转换的选项位置依赖(如“第一个选项”),先阻止重新编排并提示核对;本次 21 套样本没有因此被阻止。
+- 云端提示词只更新目标单条记录,核对旧版本与完整旧文本后写入,已准备条件回滚;新生成逻辑无论是否命中云端模板都会附加独立解析要求。
+- 原始题目顺序留存,均衡分布在共享呈现层实现;已下载的旧 PDF 必须重新导出。
+- 一次独立代码审查提出四项问题,均补充失败回归后修复:历史未支持引用、普通字母误替换、重做刷新恢复旧答案、生成解析缺少位置依赖校验。
+- 采用线上实际源码快照叠加阅读修改,保留之前报告字体发布;未带入其他候选发布。

+ 1 - 1
scripts/check-reading-print-browser.mjs

@@ -78,7 +78,7 @@ try{
  const downloadPromise=page.waitForEvent('download');await page.getByRole('button',{name:'导出为 PDF',exact:true}).click();const download=await downloadPromise;await download.saveAs(path.join(out,'verified-reading.pdf'));
  await page.locator('.pdf-document').screenshot({path:path.join(out,'print-preview.png')});
  await page.getByRole('button',{name:'返回',exact:true}).click();await page.getByRole('button',{name:'再做一次',exact:true}).click();await page.waitForURL(/presentationVersion=v1-/);
- const retry=await exerciseSnapshot(page),retryUrl=page.url();assert.notDeepEqual(retry,completed.map(({answer,...q})=>q));
+ const retry=await exerciseSnapshot(page),retryUrl=page.url();assert.notDeepEqual(retry.map(({answer,...q})=>q),completed.map(({answer,...q})=>q));
  await page.reload();await page.locator('.question-block').last().waitFor();assert.equal(await page.locator('.option.selected').count(),0);assert.deepEqual(await exerciseSnapshot(page),retry);
  await page.getByRole('button',{name:'打印',exact:true}).click();await assertPdf(page,article,retry);
  await page.getByRole('button',{name:'返回',exact:true}).click();await page.locator('.question-block').last().waitFor();assert.equal(await page.locator('.option.selected').count(),0);assert.deepEqual(await exerciseSnapshot(page),retry);