Browse Source

perf: calculate teacher earnings by month and accelerate verified reads

彭峰 11 hours ago
parent
commit
592919581e
100 changed files with 7592 additions and 34 deletions
  1. 39 0
      perf/speed-round3-2026-11-10/README.md
  2. BIN
      perf/speed-round3-2026-11-10/mobile-1-account-after-1024.png
  3. BIN
      perf/speed-round3-2026-11-10/mobile-1-account-after-390.png
  4. BIN
      perf/speed-round3-2026-11-10/mobile-1-account-before-1024.png
  5. BIN
      perf/speed-round3-2026-11-10/mobile-1-account-before-390.png
  6. BIN
      perf/speed-round3-2026-11-10/mobile-1-companion-after-1024.png
  7. BIN
      perf/speed-round3-2026-11-10/mobile-1-companion-after-390.png
  8. BIN
      perf/speed-round3-2026-11-10/mobile-1-companion-before-1024.png
  9. BIN
      perf/speed-round3-2026-11-10/mobile-1-companion-before-390.png
  10. BIN
      perf/speed-round3-2026-11-10/mobile-1-history-after-1024.png
  11. BIN
      perf/speed-round3-2026-11-10/mobile-1-history-after-390.png
  12. BIN
      perf/speed-round3-2026-11-10/mobile-1-history-before-1024.png
  13. BIN
      perf/speed-round3-2026-11-10/mobile-1-history-before-390.png
  14. BIN
      perf/speed-round3-2026-11-10/mobile-1-home-after-1024.png
  15. BIN
      perf/speed-round3-2026-11-10/mobile-1-home-after-390.png
  16. BIN
      perf/speed-round3-2026-11-10/mobile-1-home-before-1024.png
  17. BIN
      perf/speed-round3-2026-11-10/mobile-1-home-before-390.png
  18. BIN
      perf/speed-round3-2026-11-10/mobile-1-review-after-1024.png
  19. BIN
      perf/speed-round3-2026-11-10/mobile-1-review-after-390.png
  20. BIN
      perf/speed-round3-2026-11-10/mobile-1-review-before-1024.png
  21. BIN
      perf/speed-round3-2026-11-10/mobile-1-review-before-390.png
  22. BIN
      perf/speed-round3-2026-11-10/mobile-3-account-after-1024.png
  23. BIN
      perf/speed-round3-2026-11-10/mobile-3-account-after-390.png
  24. BIN
      perf/speed-round3-2026-11-10/mobile-3-account-before-1024.png
  25. BIN
      perf/speed-round3-2026-11-10/mobile-3-account-before-390.png
  26. BIN
      perf/speed-round3-2026-11-10/mobile-3-companion-after-1024.png
  27. BIN
      perf/speed-round3-2026-11-10/mobile-3-companion-after-390.png
  28. BIN
      perf/speed-round3-2026-11-10/mobile-3-companion-before-1024.png
  29. BIN
      perf/speed-round3-2026-11-10/mobile-3-companion-before-390.png
  30. BIN
      perf/speed-round3-2026-11-10/mobile-3-history-after-1024.png
  31. BIN
      perf/speed-round3-2026-11-10/mobile-3-history-after-390.png
  32. BIN
      perf/speed-round3-2026-11-10/mobile-3-history-before-1024.png
  33. BIN
      perf/speed-round3-2026-11-10/mobile-3-history-before-390.png
  34. BIN
      perf/speed-round3-2026-11-10/mobile-3-home-after-1024.png
  35. BIN
      perf/speed-round3-2026-11-10/mobile-3-home-after-390.png
  36. BIN
      perf/speed-round3-2026-11-10/mobile-3-home-before-1024.png
  37. BIN
      perf/speed-round3-2026-11-10/mobile-3-home-before-390.png
  38. BIN
      perf/speed-round3-2026-11-10/mobile-3-review-after-1024.png
  39. BIN
      perf/speed-round3-2026-11-10/mobile-3-review-after-390.png
  40. BIN
      perf/speed-round3-2026-11-10/mobile-3-review-before-1024.png
  41. BIN
      perf/speed-round3-2026-11-10/mobile-3-review-before-390.png
  42. BIN
      perf/speed-round3-2026-11-10/mobile-after-1024.png
  43. BIN
      perf/speed-round3-2026-11-10/mobile-after-390.png
  44. BIN
      perf/speed-round3-2026-11-10/mobile-before-1024.png
  45. BIN
      perf/speed-round3-2026-11-10/mobile-before-390.png
  46. 1347 0
      perf/speed-round3-2026-11-10/mobile-benchmark.json
  47. 36 0
      perf/speed-round3-2026-11-10/mobile-benchmark.mjs
  48. 297 0
      perf/speed-round3-2026-11-10/mobile-browser-paired.json
  49. 73 0
      perf/speed-round3-2026-11-10/mobile-browser-paired.mjs
  50. 50 0
      perf/speed-round3-2026-11-10/mobile-frontend-report.md
  51. 221 0
      perf/speed-round3-2026-11-10/mobile-journeys-smoke.json
  52. 50 0
      perf/speed-round3-2026-11-10/mobile-profile-summary.json
  53. 36 0
      perf/speed-round3-2026-11-10/mobile-profile.mjs
  54. 52 0
      perf/speed-round3-2026-11-10/mobile-report.md
  55. 7 0
      perf/speed-round3-2026-11-10/mobile-review-correlation.mjs
  56. 549 0
      perf/speed-round3-2026-11-10/mobile-review-plan-summary.json
  57. 11 0
      perf/speed-round3-2026-11-10/mobile-review-plan.mjs
  58. 5 0
      perf/speed-round3-2026-11-10/mobile-review-version.mjs
  59. 122 0
      perf/speed-round3-2026-11-10/mobile-visual-comparison.json
  60. 18 0
      perf/speed-round3-2026-11-10/month-ledger-baseline.json
  61. 225 0
      perf/speed-round3-2026-11-10/payroll-api-isolated-smoke.json
  62. 1899 0
      perf/speed-round3-2026-11-10/payroll-api-isolated.json
  63. 112 0
      perf/speed-round3-2026-11-10/payroll-api-isolated.mjs
  64. 22 0
      perf/speed-round3-2026-11-10/payroll-isolated-profile.mjs
  65. 142 0
      perf/speed-round3-2026-11-10/payroll-users-benchmark.json
  66. 5 0
      perf/speed-round3-2026-11-10/payroll-users-benchmark.mjs
  67. 63 0
      perf/speed-round3-2026-11-10/release.mjs
  68. 44 0
      perf/speed-round3-2026-11-10/report.md
  69. 6 0
      perf/speed-round3-2026-11-10/server-util.mjs
  70. 210 0
      perf/speed-round3-2026-11-10/source-index-concurrency.json
  71. 49 0
      perf/speed-round3-2026-11-10/source-index-report.md
  72. 282 0
      perf/speed-round3-2026-11-10/teacher-api-isolated.json
  73. 49 0
      perf/speed-round3-2026-11-10/teacher-api-isolated.mjs
  74. 66 0
      perf/speed-round3-2026-11-10/teacher-monthly-helper-metadata.json
  75. 10 0
      perf/speed-round3-2026-11-10/teacher-monthly-release.mjs
  76. 17 0
      perf/speed-round3-2026-11-10/teacher-monthly-report.md
  77. 322 0
      perf/speed-round3-2026-11-10/teacher-monthly-runtime.json
  78. 51 0
      perf/speed-round3-2026-11-10/teacher-monthly-runtime.mjs
  79. 55 0
      perf/speed-round3-2026-11-10/verify-admin-live.mjs
  80. 10 0
      perf/speed-round3-2026-11-10/verify-mobile-live.mjs
  81. 6 3
      projects/xiaoshu-mobile/src/app/core/api.service.spec.ts
  82. 1 1
      projects/xiaoshu-mobile/src/app/core/api.service.ts
  83. 1 0
      projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts
  84. 33 0
      projects/xiaoshu-mobile/src/app/core/teacher-today-review.service.spec.ts
  85. 10 3
      projects/xiaoshu-mobile/src/app/core/teacher-today-review.service.ts
  86. 1 1
      projects/xiaoshu-mobile/src/app/features/home/home-page.component.ts
  87. 5 3
      scripts/cloud/mobile-teaching.js
  88. 24 5
      scripts/cloud/payroll-integrity.js
  89. 66 10
      scripts/cloud/teacher-earnings.js
  90. 19 7
      scripts/deploy-admin-functions.mjs
  91. 62 0
      scripts/prepare-mobile-speed3-web.mjs
  92. 89 0
      scripts/speed-round3-patch.mjs
  93. 56 0
      scripts/sql/payroll-cutoff-bulk.sql
  94. 93 0
      scripts/sql/payroll-read-snapshot.sql
  95. 78 0
      scripts/sql/payroll-source-bulk.sql
  96. 348 0
      scripts/sql/payroll-source-index.sql
  97. 50 0
      scripts/sql/teacher-earned-months.sql
  98. 1 1
      scripts/teacher-earnings-transform.mjs
  99. 51 0
      scripts/tests/fixtures/mobile-read-performance-baseline.js
  100. 46 0
      scripts/tests/fixtures/mobile-review-performance-baseline.js

+ 39 - 0
perf/speed-round3-2026-11-10/README.md

@@ -0,0 +1,39 @@
+# 第三轮提速与月度计薪
+
+用户目标:费用数据变化后完整重算尽量接近 1 秒,不以旧结果掩盖延迟;学生端、老师端同步提速,验证后上线。后续明确采用按月计算:未变化的历史月份复用,补录、更正、删除、跨月移动时重算受影响月份。
+
+## 验收口径
+
+- 费用:真实汇总、完整来源核验状态、教师明细和完整导出可用。金额、来源版本、截止核验版本、异常、门店范围、历史总数均与原版一致。每轮清空普通结果缓存后比较,不能用缓存命中充当全算。
+- 月度收入:四个原有收入字段与原算法一致;历史月份不变时金额计算调用为零;单月变化只重算受影响月份。月度汇总是派生的应得收入,绝非审核或发放账本,不自动冻结草稿、不修改已结算工资。
+- 移动端:老师当天任务全部分页到齐才算可用;学生首页余额/资料准确,排课、账号、记录、复习日历保持完整内容与功能。
+- 当前数据的来源存在性、重复代表、作废、历史补充抑制、规则、审批与异常判定仍由原始记录和原规则决定。派生索引缺失或过期时实时重建,维护/规则异常时回退原读取。
+
+## 实现
+
+1. 事务快照中只构造一次核验来源,保留全部历史核验文本,仅传输所选月份候选详情;未知日期及重复身份保守保留。原始四个财务核验函数不改。
+2. 来源投影在写入时按记录维护,读取时核对当前依赖签名;当两个写入交错提交时,不信任旧投影。无公司级维护锁,争用时跳过派生写入,当前读取自行修复。
+3. 保持原排序规则及原始 MD5 字节,用一次文本展开避免比较中反复解压;仅关联所需 model 59 摘要。费用身份资料只取实际使用的显示/身份字段,保留 phone 与全部历史别名。
+4. 逐月保存整数分金额、待核对数、完整输入/计算器 SHA-256。来源与已存月份代次同属一个只读快照,条件写回防止旧计算覆盖新代次;复习收益仍实时读取。
+5. 复习查询缩小关联范围,独立 SQL 有限并发,老师多页任务有限并发;学生首页仅取所需资料,纯读取不再错误广播数据变更。
+
+## 证据与边界
+
+- `mobile-report.md` / `mobile-benchmark.json`:真实教师/学生记录的 operator-bound 只读投影,10 对接口比较。不是模拟用户登录。
+- `mobile-frontend-report.md`:固定匿名夹具下的 Angular 生产构建浏览器配对测试及两种宽度截图。不是线上真实用户分位数。
+- `teacher-monthly-report.md`:真实运行时、24 个月复用及单月更正等价性;来源索引之前的月度复用本身没有明显整体提速,因此另测组合方案。
+- `payroll-api-isolated.json` / `teacher-api-isolated.json`:隔离数据库复制数据的完整对照,计时排除项写在文件中。静态性能复制不替代真实触发器并发测试。
+- `source-index-concurrency.json`:真实 PostgreSQL 事务、真实摘要触发器、乱序提交、过期投影和写入成本;所有修改仅在随机私有 schema 中,测试后删除。
+- `admin-release-verification.json` / `mobile-release-verification.json`:发布后的公网与页面回读(完成后生成)。单次上线冒烟不冒充 p75。
+
+源码提交必须只包含本批文件;不提交本地原始响应、账户数据、凭据、完整数据库副本。私有候选、回滚备份和发布凭据位于 `/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3`。
+
+`payroll-source-bulk.sql` 与对应测试保留为早期实验复现,最终运行路径不依赖它,因此不在发布迁移清单中。
+
+## 发布与回滚
+
+`release.mjs prepare` 只生成候选、精确函数白名单审计及 SQL;`install` 在锁超时保护的事务中预检同名对象、原有 39 函数与摘要健康状态,只安装本批派生结构;`rehearse` 实际演练删除派生对象并回滚事务。`publish` 校验线上基线后只替换两网关白名单函数,并回读校验。
+
+静态只发布移动端 H5;原生安装包不在本批范围。先上传带内容哈希的新资源,逐文件验证,再切换 manifest/index 并刷新现有 CDN。管理端本批没有静态源码变化。
+
+回滚顺序:先在网关当前哈希仍等于本批发布哈希时恢复私有 `admin-gateway.json`、`mobile-gateway.json` 原代码并回读;再恢复 mobile/baseline 的 manifest/index、刷新并回读。确认没有本批网关在使用新结构后,执行 `release.mjs rollback`,只删本批触发器、派生表、函数及索引;不使用 CASCADE,未知依赖会阻止回滚。新派生对象删除前后均比较原有函数定义。任何来源业务表、工资审核/付款记录均不属于回滚删除范围。

BIN
perf/speed-round3-2026-11-10/mobile-1-account-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-account-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-account-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-account-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-companion-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-companion-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-companion-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-companion-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-history-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-history-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-history-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-history-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-home-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-home-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-home-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-home-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-review-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-review-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-1-review-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-1-review-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-account-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-account-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-account-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-account-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-companion-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-companion-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-companion-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-companion-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-history-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-history-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-history-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-history-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-home-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-home-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-home-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-home-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-review-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-review-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-3-review-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-3-review-before-390.png


BIN
perf/speed-round3-2026-11-10/mobile-after-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-after-390.png


BIN
perf/speed-round3-2026-11-10/mobile-before-1024.png


BIN
perf/speed-round3-2026-11-10/mobile-before-390.png


+ 1347 - 0
perf/speed-round3-2026-11-10/mobile-benchmark.json

@@ -0,0 +1,1347 @@
+{
+  "context": "Operator-bound projections of existing teacher/student records, not role-login acceptance. Ten sequential alternating AB samples per case. Fresh SQL each call; no business-result cache. Student profile compares profile only; legacy synchronization is replaced with a no-write stub in both variants. SQL duration excludes current-user Parse fetch; request time includes it and transport.",
+  "summary": [
+    {
+      "case": "teacherCompanion",
+      "baseline": {
+        "samples": 10,
+        "p50Ms": 71,
+        "p75Ms": 82,
+        "p95Ms": 90,
+        "requestP75Ms": 234,
+        "queries": 5
+      },
+      "candidate": {
+        "samples": 10,
+        "p50Ms": 47,
+        "p75Ms": 50,
+        "p95Ms": 101,
+        "requestP75Ms": 164,
+        "queries": 5
+      },
+      "p75Reduction": 0.3902439024390244
+    },
+    {
+      "case": "teacherReview",
+      "baseline": {
+        "samples": 10,
+        "p50Ms": 1500,
+        "p75Ms": 1701,
+        "p95Ms": 1819,
+        "requestP75Ms": 1819,
+        "queries": 5
+      },
+      "candidate": {
+        "samples": 10,
+        "p50Ms": 59,
+        "p75Ms": 62,
+        "p95Ms": 87,
+        "requestP75Ms": 246,
+        "queries": 5
+      },
+      "p75Reduction": 0.9635508524397414
+    },
+    {
+      "case": "studentCompanion",
+      "baseline": {
+        "samples": 10,
+        "p50Ms": 46,
+        "p75Ms": 49,
+        "p95Ms": 53,
+        "requestP75Ms": 211,
+        "queries": 5
+      },
+      "candidate": {
+        "samples": 10,
+        "p50Ms": 30,
+        "p75Ms": 32,
+        "p95Ms": 37,
+        "requestP75Ms": 204,
+        "queries": 5
+      },
+      "p75Reduction": 0.34693877551020413
+    },
+    {
+      "case": "studentProfile",
+      "baseline": {
+        "samples": 10,
+        "p50Ms": 97,
+        "p75Ms": 100,
+        "p95Ms": 121,
+        "requestP75Ms": 223,
+        "queries": 5
+      },
+      "candidate": {
+        "samples": 10,
+        "p50Ms": 1,
+        "p75Ms": 1,
+        "p95Ms": 1,
+        "requestP75Ms": 157,
+        "queries": 0
+      },
+      "p75Reduction": 0.99
+    }
+  ],
+  "metrics": [
+    {
+      "case": "teacherCompanion",
+      "round": 0,
+      "variant": "baseline",
+      "durationMs": 81,
+      "requestMs": 305,
+      "queryCount": 5,
+      "queryMs": [
+        19,
+        18,
+        12,
+        13,
+        16
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 0,
+      "variant": "candidate",
+      "durationMs": 101,
+      "requestMs": 513,
+      "queryCount": 5,
+      "queryMs": [
+        35,
+        66,
+        32,
+        86,
+        14
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 1,
+      "variant": "baseline",
+      "durationMs": 82,
+      "requestMs": 206,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        16,
+        19,
+        14,
+        17
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 1,
+      "variant": "candidate",
+      "durationMs": 45,
+      "requestMs": 158,
+      "queryCount": 5,
+      "queryMs": [
+        21,
+        21,
+        23,
+        12,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 2,
+      "variant": "baseline",
+      "durationMs": 83,
+      "requestMs": 193,
+      "queryCount": 5,
+      "queryMs": [
+        28,
+        16,
+        14,
+        12,
+        13
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 2,
+      "variant": "candidate",
+      "durationMs": 42,
+      "requestMs": 175,
+      "queryCount": 5,
+      "queryMs": [
+        17,
+        19,
+        26,
+        13,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 3,
+      "variant": "baseline",
+      "durationMs": 69,
+      "requestMs": 179,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        15,
+        13,
+        12,
+        11
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 3,
+      "variant": "candidate",
+      "durationMs": 48,
+      "requestMs": 154,
+      "queryCount": 5,
+      "queryMs": [
+        17,
+        21,
+        23,
+        17,
+        14
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 4,
+      "variant": "baseline",
+      "durationMs": 66,
+      "requestMs": 167,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        14,
+        12,
+        12,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 4,
+      "variant": "candidate",
+      "durationMs": 44,
+      "requestMs": 150,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        20,
+        22,
+        15,
+        13
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 5,
+      "variant": "baseline",
+      "durationMs": 68,
+      "requestMs": 184,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        15,
+        12,
+        12,
+        13
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 5,
+      "variant": "candidate",
+      "durationMs": 50,
+      "requestMs": 155,
+      "queryCount": 5,
+      "queryMs": [
+        20,
+        26,
+        31,
+        17,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 6,
+      "variant": "baseline",
+      "durationMs": 71,
+      "requestMs": 234,
+      "queryCount": 5,
+      "queryMs": [
+        17,
+        15,
+        13,
+        12,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 6,
+      "variant": "candidate",
+      "durationMs": 47,
+      "requestMs": 156,
+      "queryCount": 5,
+      "queryMs": [
+        12,
+        27,
+        29,
+        22,
+        13
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 7,
+      "variant": "baseline",
+      "durationMs": 71,
+      "requestMs": 186,
+      "queryCount": 5,
+      "queryMs": [
+        17,
+        16,
+        12,
+        13,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 7,
+      "variant": "candidate",
+      "durationMs": 41,
+      "requestMs": 161,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        19,
+        25,
+        12,
+        13
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 8,
+      "variant": "baseline",
+      "durationMs": 73,
+      "requestMs": 190,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        19,
+        12,
+        14,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 8,
+      "variant": "candidate",
+      "durationMs": 51,
+      "requestMs": 164,
+      "queryCount": 5,
+      "queryMs": [
+        16,
+        26,
+        32,
+        19,
+        16
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 9,
+      "variant": "baseline",
+      "durationMs": 90,
+      "requestMs": 325,
+      "queryCount": 5,
+      "queryMs": [
+        17,
+        15,
+        30,
+        16,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherCompanion",
+      "round": 9,
+      "variant": "candidate",
+      "durationMs": 48,
+      "requestMs": 155,
+      "queryCount": 5,
+      "queryMs": [
+        20,
+        27,
+        27,
+        16,
+        12
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "teacherReview",
+      "round": 0,
+      "variant": "baseline",
+      "durationMs": 1456,
+      "requestMs": 1555,
+      "queryCount": 5,
+      "queryMs": [
+        79,
+        189,
+        995,
+        1394,
+        1453
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 0,
+      "variant": "candidate",
+      "durationMs": 67,
+      "requestMs": 251,
+      "queryCount": 5,
+      "queryMs": [
+        14,
+        20,
+        28,
+        45,
+        65
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 1,
+      "variant": "baseline",
+      "durationMs": 1819,
+      "requestMs": 1924,
+      "queryCount": 5,
+      "queryMs": [
+        38,
+        302,
+        1588,
+        1640,
+        1815
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 1,
+      "variant": "candidate",
+      "durationMs": 57,
+      "requestMs": 201,
+      "queryCount": 5,
+      "queryMs": [
+        5,
+        8,
+        13,
+        45,
+        55
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 2,
+      "variant": "baseline",
+      "durationMs": 1503,
+      "requestMs": 1684,
+      "queryCount": 5,
+      "queryMs": [
+        18,
+        194,
+        1385,
+        1438,
+        1502
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 2,
+      "variant": "candidate",
+      "durationMs": 59,
+      "requestMs": 175,
+      "queryCount": 5,
+      "queryMs": [
+        6,
+        10,
+        15,
+        50,
+        58
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 3,
+      "variant": "baseline",
+      "durationMs": 1661,
+      "requestMs": 1786,
+      "queryCount": 5,
+      "queryMs": [
+        33,
+        210,
+        1485,
+        1612,
+        1659
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 3,
+      "variant": "candidate",
+      "durationMs": 57,
+      "requestMs": 195,
+      "queryCount": 5,
+      "queryMs": [
+        8,
+        15,
+        17,
+        53,
+        56
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 4,
+      "variant": "baseline",
+      "durationMs": 1500,
+      "requestMs": 1615,
+      "queryCount": 5,
+      "queryMs": [
+        30,
+        178,
+        1438,
+        1496,
+        1500
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 4,
+      "variant": "candidate",
+      "durationMs": 62,
+      "requestMs": 191,
+      "queryCount": 5,
+      "queryMs": [
+        4,
+        13,
+        19,
+        53,
+        61
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 5,
+      "variant": "baseline",
+      "durationMs": 1441,
+      "requestMs": 1556,
+      "queryCount": 5,
+      "queryMs": [
+        15,
+        155,
+        1359,
+        1413,
+        1440
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 5,
+      "variant": "candidate",
+      "durationMs": 62,
+      "requestMs": 244,
+      "queryCount": 5,
+      "queryMs": [
+        4,
+        17,
+        20,
+        60,
+        60
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 6,
+      "variant": "baseline",
+      "durationMs": 1701,
+      "requestMs": 1819,
+      "queryCount": 5,
+      "queryMs": [
+        10,
+        173,
+        1489,
+        1605,
+        1700
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 6,
+      "variant": "candidate",
+      "durationMs": 87,
+      "requestMs": 279,
+      "queryCount": 5,
+      "queryMs": [
+        6,
+        18,
+        27,
+        79,
+        87
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 7,
+      "variant": "baseline",
+      "durationMs": 1483,
+      "requestMs": 1587,
+      "queryCount": 5,
+      "queryMs": [
+        30,
+        198,
+        1333,
+        1444,
+        1482
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 7,
+      "variant": "candidate",
+      "durationMs": 55,
+      "requestMs": 159,
+      "queryCount": 5,
+      "queryMs": [
+        3,
+        6,
+        20,
+        47,
+        55
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 8,
+      "variant": "baseline",
+      "durationMs": 1469,
+      "requestMs": 1582,
+      "queryCount": 5,
+      "queryMs": [
+        14,
+        190,
+        1385,
+        1460,
+        1468
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 8,
+      "variant": "candidate",
+      "durationMs": 54,
+      "requestMs": 171,
+      "queryCount": 5,
+      "queryMs": [
+        7,
+        9,
+        13,
+        45,
+        52
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 9,
+      "variant": "baseline",
+      "durationMs": 1790,
+      "requestMs": 1928,
+      "queryCount": 5,
+      "queryMs": [
+        19,
+        133,
+        1695,
+        1772,
+        1788
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "teacherReview",
+      "round": 9,
+      "variant": "candidate",
+      "durationMs": 62,
+      "requestMs": 246,
+      "queryCount": 5,
+      "queryMs": [
+        4,
+        16,
+        30,
+        47,
+        61
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "studentCompanion",
+      "round": 0,
+      "variant": "baseline",
+      "durationMs": 53,
+      "requestMs": 277,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        12,
+        11,
+        7,
+        11
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 0,
+      "variant": "candidate",
+      "durationMs": 32,
+      "requestMs": 204,
+      "queryCount": 5,
+      "queryMs": [
+        17,
+        19,
+        3,
+        21,
+        11
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 1,
+      "variant": "baseline",
+      "durationMs": 48,
+      "requestMs": 164,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        12,
+        12,
+        3,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 1,
+      "variant": "candidate",
+      "durationMs": 37,
+      "requestMs": 149,
+      "queryCount": 5,
+      "queryMs": [
+        13,
+        3,
+        19,
+        26,
+        11
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 2,
+      "variant": "baseline",
+      "durationMs": 45,
+      "requestMs": 154,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        10,
+        11,
+        3,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 2,
+      "variant": "candidate",
+      "durationMs": 28,
+      "requestMs": 193,
+      "queryCount": 5,
+      "queryMs": [
+        12,
+        14,
+        3,
+        17,
+        11
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 3,
+      "variant": "baseline",
+      "durationMs": 45,
+      "requestMs": 162,
+      "queryCount": 5,
+      "queryMs": [
+        10,
+        11,
+        11,
+        3,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 3,
+      "variant": "candidate",
+      "durationMs": 30,
+      "requestMs": 130,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        14,
+        4,
+        19,
+        11
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 4,
+      "variant": "baseline",
+      "durationMs": 49,
+      "requestMs": 228,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        10,
+        11,
+        6,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 4,
+      "variant": "candidate",
+      "durationMs": 27,
+      "requestMs": 153,
+      "queryCount": 5,
+      "queryMs": [
+        12,
+        14,
+        3,
+        17,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 5,
+      "variant": "baseline",
+      "durationMs": 45,
+      "requestMs": 166,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        10,
+        12,
+        2,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 5,
+      "variant": "candidate",
+      "durationMs": 30,
+      "requestMs": 201,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        2,
+        13,
+        19,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 6,
+      "variant": "baseline",
+      "durationMs": 52,
+      "requestMs": 211,
+      "queryCount": 5,
+      "queryMs": [
+        15,
+        12,
+        12,
+        3,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 6,
+      "variant": "candidate",
+      "durationMs": 34,
+      "requestMs": 184,
+      "queryCount": 5,
+      "queryMs": [
+        12,
+        7,
+        21,
+        23,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 7,
+      "variant": "baseline",
+      "durationMs": 48,
+      "requestMs": 162,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        11,
+        11,
+        3,
+        12
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 7,
+      "variant": "candidate",
+      "durationMs": 32,
+      "requestMs": 140,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        14,
+        4,
+        22,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 8,
+      "variant": "baseline",
+      "durationMs": 46,
+      "requestMs": 151,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        11,
+        11,
+        3,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 8,
+      "variant": "candidate",
+      "durationMs": 29,
+      "requestMs": 204,
+      "queryCount": 5,
+      "queryMs": [
+        12,
+        15,
+        4,
+        19,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 9,
+      "variant": "baseline",
+      "durationMs": 46,
+      "requestMs": 163,
+      "queryCount": 5,
+      "queryMs": [
+        11,
+        10,
+        12,
+        3,
+        10
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentCompanion",
+      "round": 9,
+      "variant": "candidate",
+      "durationMs": 29,
+      "requestMs": 224,
+      "queryCount": 5,
+      "queryMs": [
+        10,
+        3,
+        14,
+        18,
+        11
+      ],
+      "rows": 20,
+      "total": 26
+    },
+    {
+      "case": "studentProfile",
+      "round": 0,
+      "variant": "baseline",
+      "durationMs": 100,
+      "requestMs": 209,
+      "queryCount": 5,
+      "queryMs": [
+        7,
+        16,
+        21,
+        48,
+        82
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 0,
+      "variant": "candidate",
+      "durationMs": 0,
+      "requestMs": 115,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 1,
+      "variant": "baseline",
+      "durationMs": 89,
+      "requestMs": 197,
+      "queryCount": 5,
+      "queryMs": [
+        5,
+        12,
+        10,
+        28,
+        77
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 1,
+      "variant": "candidate",
+      "durationMs": 0,
+      "requestMs": 136,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 2,
+      "variant": "baseline",
+      "durationMs": 97,
+      "requestMs": 204,
+      "queryCount": 5,
+      "queryMs": [
+        4,
+        11,
+        23,
+        25,
+        85
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 2,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 186,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 3,
+      "variant": "baseline",
+      "durationMs": 96,
+      "requestMs": 200,
+      "queryCount": 5,
+      "queryMs": [
+        5,
+        12,
+        17,
+        21,
+        84
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 3,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 105,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 4,
+      "variant": "baseline",
+      "durationMs": 121,
+      "requestMs": 233,
+      "queryCount": 5,
+      "queryMs": [
+        7,
+        12,
+        18,
+        21,
+        108
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 4,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 119,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 5,
+      "variant": "baseline",
+      "durationMs": 94,
+      "requestMs": 220,
+      "queryCount": 5,
+      "queryMs": [
+        6,
+        12,
+        17,
+        23,
+        82
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 5,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 171,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 6,
+      "variant": "baseline",
+      "durationMs": 90,
+      "requestMs": 200,
+      "queryCount": 5,
+      "queryMs": [
+        5,
+        12,
+        12,
+        29,
+        77
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 6,
+      "variant": "candidate",
+      "durationMs": 0,
+      "requestMs": 104,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 7,
+      "variant": "baseline",
+      "durationMs": 99,
+      "requestMs": 223,
+      "queryCount": 5,
+      "queryMs": [
+        4,
+        11,
+        21,
+        21,
+        87
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 7,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 131,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 8,
+      "variant": "baseline",
+      "durationMs": 111,
+      "requestMs": 229,
+      "queryCount": 5,
+      "queryMs": [
+        6,
+        11,
+        17,
+        30,
+        100
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 8,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 157,
+      "queryCount": 0,
+      "queryMs": []
+    },
+    {
+      "case": "studentProfile",
+      "round": 9,
+      "variant": "baseline",
+      "durationMs": 98,
+      "requestMs": 209,
+      "queryCount": 5,
+      "queryMs": [
+        4,
+        11,
+        18,
+        24,
+        86
+      ]
+    },
+    {
+      "case": "studentProfile",
+      "round": 9,
+      "variant": "candidate",
+      "durationMs": 1,
+      "requestMs": 108,
+      "queryCount": 0,
+      "queryMs": []
+    }
+  ]
+}

+ 36 - 0
perf/speed-round3-2026-11-10/mobile-benchmark.mjs

@@ -0,0 +1,36 @@
+import fs from 'node:fs';import ts from 'typescript';import assert from 'node:assert/strict';import {withGateway} from './server-util.mjs';
+const privateDir='tmp/speed-round3-2026-11-10',outDir='perf/speed-round3-2026-11-10';fs.mkdirSync(privateDir,{recursive:true});
+const baseline=JSON.parse(fs.readFileSync('/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3/mobile-gateway.json','utf8')).code;
+const local=fs.readFileSync('scripts/deploy-admin-functions.mjs','utf8').match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1]+'\n'+fs.readFileSync('scripts/cloud/mobile-teaching.js','utf8');
+const parsed=ts.createSourceFile('gateway.js',local,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);
+function helper(name){const node=parsed.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name?.text===name);if(!node)throw Error('Missing helper '+name);return node.getText(parsed).replace('function '+name+'(','function candidate'+name+'(');}
+const company=baseline.match(/const DEFAULT_COMPANY_ID\s*=\s*['"]([^'"]+)/)?.[1];if(!company)throw Error('Missing tenant');
+const subjectSql=`WITH counts AS (SELECT szyh AS uid,COUNT(*)::int appointments FROM "CourseAppointment" WHERE company=$1 GROUP BY szyh) SELECT u."objectId",c.appointments FROM counts c JOIN "_User" u ON u.company=$1 AND u."legacyUserId"::text=c.uid WHERE u."legacyGroupId"=1 AND COALESCE(u."isDeleted",false)=false ORDER BY c.appointments DESC LIMIT 1`;
+const teacher=JSON.parse(fs.readFileSync(privateDir+'/mobile-subjects.json','utf8'))[0];
+const students=await withGateway(`async function run(){return await Psql.query(${JSON.stringify(subjectSql)},[${JSON.stringify(company)}]);}`,async call=>call({}));if(!students.length)throw Error('No real student subject');
+fs.writeFileSync(privateDir+'/mobile-student-subject.json',JSON.stringify(students),{mode:0o600});
+const additions=[helper('appCompanionOverview'),helper('appAccountOverview'),helper('appLiveScopeVersion'),helper('appLiveVersionSnapshot').replace('await appLiveScopeVersion(','await candidateappLiveScopeVersion('),helper('appReviewPage').replace('appLiveVersionSnapshot(','candidateappLiveVersionSnapshot(')].join('\n');
+const instrumentation=`let mobileSqlCalls=[];const MOBILE_PROFILE_DB=new Proxy({},{get(_target,name){const target=Psql;if(!['query','one','oneOrNone'].includes(name))return target[name];return async(sql,args)=>{const started=Date.now();const value=await target[name](sql,args);mobileSqlCalls.push({method:name,durationMs:Date.now()-started,rows:Array.isArray(value)?value.length:1});return value;};}});`;
+const source=instrumentation+'\n'+(baseline+'\n'+additions).replace(/\bPsql\b/g,'MOBILE_PROFILE_DB')+`
+// Projection excludes legacy synchronization: it must never write business data.
+async function syncLegacyReviews(){return{state:'read-only-projection'};}
+async function run(input){
+ const cases={teacherCompanion:{subject:${JSON.stringify(teacher.objectId)},helper:'companion',params:{source:'database',page:1,pageSize:20,dateStart:'2026-11-10',dateEnd:'2026-11-10'}},teacherReview:{subject:${JSON.stringify(teacher.objectId)},helper:'review',params:{scope:'today',page:1,pageSize:100,includeMetadata:false,includeIncome:false}},studentCompanion:{subject:${JSON.stringify(students[0].objectId)},helper:'companion',params:{source:'database',month:'2026-11',page:1,pageSize:20}},studentProfile:{subject:${JSON.stringify(students[0].objectId)},helper:'profile',params:{source:'database',profileOnly:true}}};
+ const selected=cases[input.case];if(!selected)throw Error('Unknown fixed case');
+ const current=await new Parse.Query('_User').get(selected.subject,{useMasterKey:true});
+ const functions=input.variant==='candidate'?{companion:candidateappCompanionOverview,review:candidateappReviewPage,profile:candidateappAccountOverview}:{companion:appCompanionOverview,review:appReviewPage,profile:appAccountOverview};
+ mobileSqlCalls=[];const started=Date.now(),result=await functions[selected.helper](selected.params,current);return{durationMs:Date.now()-started,queries:mobileSqlCalls,result};
+}`;
+const cases=process.argv.includes('--smoke')?['teacherReview']:['teacherCompanion','teacherReview','studentCompanion','studentProfile'],rounds=process.argv.includes('--smoke')?1:10;
+const raw=[],metrics=[];
+function canonical(value){return JSON.parse(JSON.stringify(value,(key,v)=>['refreshedAt','serverNow','lagSeconds'].includes(key)?undefined:v));}
+await withGateway(source,async call=>{
+ for(const name of cases)for(let round=0;round<rounds;round++){
+  const pair={};for(const variant of ['baseline','candidate']){const start=performance.now(),value=await call({case:name,variant});const row={case:name,round,variant,durationMs:value.durationMs,requestMs:Math.round(performance.now()-start),queryCount:value.queries.length,queryMs:value.queries.map(q=>q.durationMs),rows:value.result.schedules?.length??value.result.items?.length,total:value.result.page?.itemCount};metrics.push(row);raw.push({...row,...value});pair[variant]=name==='studentProfile'?value.result.profile:value.result;console.log(JSON.stringify(row));}
+  assert.deepEqual(canonical(pair.candidate),canonical(pair.baseline),name+' output changed in round '+round);
+ }
+});
+fs.writeFileSync(privateDir+'/mobile-benchmark-raw.json',JSON.stringify(raw,null,2),{mode:0o600});
+const quantile=(a,p)=>a.toSorted((a,b)=>a-b)[Math.ceil(a.length*p)-1];
+const summary=cases.map(name=>{const value={case:name};for(const variant of ['baseline','candidate']){const rows=metrics.filter(r=>r.case===name&&r.variant===variant);value[variant]={samples:rows.length,p50Ms:quantile(rows.map(r=>r.durationMs),.5),p75Ms:quantile(rows.map(r=>r.durationMs),.75),p95Ms:quantile(rows.map(r=>r.durationMs),.95),requestP75Ms:quantile(rows.map(r=>r.requestMs),.75),queries:rows[0].queryCount};}value.p75Reduction=1-value.candidate.p75Ms/value.baseline.p75Ms;return value;});
+const filename=process.argv.includes('--smoke')?'mobile-candidate-smoke.json':'mobile-benchmark.json';fs.writeFileSync(outDir+'/'+filename,JSON.stringify({context:'Operator-bound projections of existing teacher/student records, not role-login acceptance. Ten sequential alternating AB samples per case. Fresh SQL each call; no business-result cache. Student profile compares profile only; legacy synchronization is replaced with a no-write stub in both variants. SQL duration excludes current-user Parse fetch; request time includes it and transport.',summary,metrics},null,2));console.log(JSON.stringify({summary}));

+ 297 - 0
perf/speed-round3-2026-11-10/mobile-browser-paired.json

@@ -0,0 +1,297 @@
+{
+  "scope": "Isolated actual Angular production browser builds; 301 same-day tasks, 400 ms each fixture API; fresh browser/login/cache; gzip, 20 ms 4 Mbps; complete ordered list, same stats and enabled controls; not production real-user measurement",
+  "summary": {
+    "before": {
+      "p50": 2264,
+      "p75": 2295,
+      "p95": 2367
+    },
+    "after": {
+      "p50": 1456,
+      "p75": 1462,
+      "p95": 1504
+    }
+  },
+  "records": [
+    {
+      "case": "3-home",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 2367,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 1445,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 1,
+      "variant": "after",
+      "readyMs": 1459,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 1,
+      "variant": "before",
+      "readyMs": 2270,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 2,
+      "variant": "before",
+      "readyMs": 2276,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 2,
+      "variant": "after",
+      "readyMs": 1463,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 3,
+      "variant": "after",
+      "readyMs": 1462,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 3,
+      "variant": "before",
+      "readyMs": 2253,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 4,
+      "variant": "before",
+      "readyMs": 2259,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 4,
+      "variant": "after",
+      "readyMs": 1504,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 5,
+      "variant": "after",
+      "readyMs": 1454,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 5,
+      "variant": "before",
+      "readyMs": 2256,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 6,
+      "variant": "before",
+      "readyMs": 2300,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 6,
+      "variant": "after",
+      "readyMs": 1457,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 7,
+      "variant": "after",
+      "readyMs": 1442,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 7,
+      "variant": "before",
+      "readyMs": 2258,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 8,
+      "variant": "before",
+      "readyMs": 2295,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 8,
+      "variant": "after",
+      "readyMs": 1456,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 9,
+      "variant": "after",
+      "readyMs": 1439,
+      "maxConcurrentReviewReads": 3,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 9,
+      "variant": "before",
+      "readyMs": 2264,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page",
+        "app_review_page"
+      ]
+    }
+  ]
+}

+ 73 - 0
perf/speed-round3-2026-11-10/mobile-browser-paired.mjs

@@ -0,0 +1,73 @@
+import {chromium} from 'playwright';
+import {createServer} from 'node:http';
+import fs from 'node:fs';
+import {resolve,extname} from 'node:path';
+import {gzipSync} from 'node:zlib';
+import assert from 'node:assert/strict';
+import {fileURLToPath} from 'node:url';
+const root=process.cwd(),out=fileURLToPath(new URL('.',import.meta.url));
+const baseline=process.argv[2];if(!baseline)throw Error('Pass the unchanged baseline mobile browser build directory');
+const journeys=process.env.MOBILE_JOURNEYS==='1';
+const count=journeys?3:301,runs=Number(process.env.PERF_RUNS||(journeys?1:10)),servers=[],origins={},records=[],concurrency={};
+const date=new Intl.DateTimeFormat('en-CA',{timeZone:'Asia/Shanghai'}).format(new Date());
+const refreshedAt=date+'T08:00:00.000Z';
+const course={GeneralID:801,kcid:801,bindingId:'binding-801',Title:'初中英语|七年级上册@同步词汇',courseName:'七年级上册',enrollmentStatus:'active'};
+const schedule={GeneralID:901,bindingId:'binding-801',szyh:100,kcid:801,bxrq:date,sdsd:'23:00',Title:'同步伴学',plxm:'测试老师',dszt:0};
+const profile={userId:100,groupId:1,honeyName:'测试学员',UserPoint:360,purse:20,silverCoin:10};
+const cases=journeys?[1,3].flatMap(role=>['home','companion','account','history','review'].map(name=>({role,name,path:({home:'/pages/home/index',companion:role===1?'/pages/home/cart':'/pages/home/shop',account:'/pages/home/ucenter',history:'/pages/his/his',review:'/pages/stu/stu_calendar'})[name]}))):[{role:3,name:'home',path:'/pages/home/index'}];
+for(const [variant,dir] of Object.entries({before:resolve(baseline),after:root+'/dist/xiaoshu-mobile/browser'})){
+ concurrency[variant]={active:0,max:0};
+ const server=createServer(async(req,res)=>{
+  const pathname=new URL(req.url,'http://local').pathname;
+  if(pathname.startsWith('/cloud-functions/')){
+   let body='';for await(const chunk of req)body+=chunk;const p=JSON.parse(body).params;let responsePage;
+   let result;
+   if(p.action==='e_order_tongji')result={t30:4,t60:3,t_tiyan:1,t_total:8,t_shichang:300,totalIncome:100};
+   else if(p.action==='app_review_page'){
+    concurrency[variant].active++;concurrency[variant].max=Math.max(concurrency[variant].max,concurrency[variant].active);
+    await new Promise(r=>setTimeout(r,400));
+    const page=Number(p.page),offset=(page-1)*100;
+    result={serverToday:date,items:Array.from({length:Math.min(100,count-offset)},(_,i)=>({GeneralID:offset+i+1,yhid:100,Title:'今日学员'+(offset+i+1),kc_title:'同步词汇',kywsj:date+' 09:00',fxzt:0})),page:{itemCount:count,pageCount:Math.ceil(count/100)},summary:{total:count,today:count,due:count,completed:0,income:0},dates:[{date,total:count,completed:0}],students:[{id:'100',name:'测试学员',taskCount:count}]};
+   }else if(p.action==='app_account_overview')result={profile:p.uid===300?{userId:300,groupId:3,honeyName:'测试老师'}:profile,refreshedAt,coachPanel:{todaySchedules:1,completedLessons:4,reviewIncome:20,lessonIncome:80,totalIncome:100,reviewRecords:5,totalLessons:4,totalHours:3,course30:2,course60:2,trialLessons:0},companionSummary:{completedSessions:4,completedMinutes:180},antiForgetSummary:{total:3,completed:0,progress:0,todayTotal:3,todayCompleted:0,duePending:3,scheduled:3,scheduledDays:1}};
+   else if(p.action==='app_companion_overview')result={courses:[course],schedules:[schedule],dates:[{date,finished:0,planned:1}],studentOptions:[{id:'100',name:'测试学员',count:1}],page:{itemCount:1,pageCount:1},refreshedAt,syncStatus:{state:'current'}};
+   else if(p.action==='app_learning_overview')result={records:[{GeneralID:201,CreateTime:date+' 09:30:00',con:'初中英语@Unit 1'}],courses:[course],schedules:[schedule],recordDateCounts:{[date]:1},scheduleDateCounts:{[date]:{planned:1,missed:0}},page:{itemCount:1,pageCount:1},refreshedAt,syncStatus:{state:'current'}};
+   else if(p.action==='e_user_list'){result=[{UserID:100,HoneyName:'测试学员'}];responsePage={itemCount:1,pageCount:1};}
+   else if(p.action==='app_support_requests_list')result={unread:0,items:[],page:{itemCount:0,pageCount:1}};
+   else if(p.action==='app_live_versions')result={versions:{learning:'1',courses:'1',appointments:'1',reading:'1'},syncStatus:{state:'current'}};
+   else {res.writeHead(400);res.end(JSON.stringify({retcode:-1,retmsg:'Unexpected fixture '+p.action}));return;}
+   if(p.action==='app_review_page')concurrency[variant].active--;
+   res.setHeader('Content-Type','application/json');res.end(JSON.stringify({retcode:0,result,...(responsePage?{page:responsePage}:{})}));return;
+  }
+  let file=resolve(dir,'.'+decodeURIComponent(pathname));if(!file.startsWith(dir+'/')||!fs.existsSync(file)||!extname(file))file=dir+'/index.html';
+  const data=fs.readFileSync(file);res.setHeader('Content-Type',({'.js':'text/javascript','.css':'text/css','.html':'text/html','.png':'image/png','.svg':'image/svg+xml'})[extname(file)]||'application/octet-stream');
+  res.setHeader('Content-Encoding','gzip');res.end(gzipSync(data));
+ });await new Promise(r=>server.listen(0,'127.0.0.1',r));servers.push(server);origins[variant]='http://127.0.0.1:'+server.address().port;
+}
+const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
+try{
+ for(const testCase of cases)for(let run=0;run<runs;run++)for(const variant of run%2?['after','before']:['before','after']){
+  const context=await browser.newContext({viewport:{width:390,height:844}}),page=await context.newPage(),origin=origins[variant],errors=[],reads=[];
+  concurrency[variant].max=0;
+  await context.addInitScript(role=>localStorage.setItem('xiaoshu.angular.session',JSON.stringify({token:'r:isolated-fixture',user:{userId:role===3?300:100,groupId:role,honeyName:role===3?'测试老师':'测试学员'}})),testCase.role);
+  await context.route('**/*',r=>new URL(r.request().url()).origin===origin?r.continue():r.abort());
+  page.on('pageerror',e=>errors.push(e.message));page.on('request',r=>{if(r.url().includes('/cloud-functions/'))reads.push(r.postDataJSON().params);});
+  const cdp=await context.newCDPSession(page);await cdp.send('Network.enable');await cdp.send('Network.emulateNetworkConditions',{offline:false,latency:20,downloadThroughput:4*1024*1024/8,uploadThroughput:3*1024*1024/8});
+  const start=performance.now();await page.goto(origin+testCase.path,{waitUntil:'domcontentloaded'});
+  await page.waitForFunction(({count,name,role})=>{
+   if(name==='home')return role===3?document.querySelectorAll('.coach-memory-list article').length===count&&!document.querySelector('.coach-memory-refresh')?.disabled&&!document.querySelector('.coach-hero .coach-refresh')?.disabled:document.querySelector('.memory-content')&&!document.querySelector('.student-home-refresh')?.disabled&&document.querySelector('.student-stats')?.textContent.includes('360');
+   if(name==='companion')return document.querySelector('.schedule-band')&&!document.querySelector('.schedule-actions button')?.disabled&&document.querySelector('.schedule-band')?.textContent.includes('同步伴学');
+   if(name==='account')return document.querySelector('.membership-card')&&!document.body.textContent.includes('正在同步')&&document.body.textContent.includes(role===3?'¥100.00':'360');
+   if(name==='history')return document.querySelectorAll('.learning-record-card .learning-item').length===1&&!document.querySelector('select[name="student"]')?.disabled;
+   return document.querySelectorAll('.calendar-list article').length===count;
+  },{count,...testCase});
+  const readyMs=Math.round(performance.now()-start),text=await page.locator('app-shell').innerText(),maxConcurrentReviewReads=concurrency[variant].max;
+  if(!journeys){assert.equal(reads.filter(p=>p.action==='app_review_page').length,4);assert.equal(maxConcurrentReviewReads,variant==='before'?1:3);}assert.deepEqual(errors,[]);
+  if(testCase.name==='home'&&testCase.role===1&&variant==='after')assert.equal(reads.find(p=>p.action==='app_account_overview').profileOnly,true);
+  if(run===0)for(const width of [390,1024]){await page.setViewportSize({width,height:844});await page.waitForTimeout(200);await page.screenshot({path:out+'/mobile-'+(journeys?testCase.role+'-'+testCase.name+'-':'')+variant+'-'+width+'.png',animations:'disabled'});}
+  records.push({case:testCase.role+'-'+testCase.name,run,variant,readyMs,maxConcurrentReviewReads,actions:reads.map(p=>p.action),text});console.log(JSON.stringify({case:testCase.role+'-'+testCase.name,run,variant,readyMs}));await context.close();
+ }
+ for(const testCase of cases)for(let run=0;run<runs;run++){const key=testCase.role+'-'+testCase.name;assert.equal(records.find(r=>r.case===key&&r.run===run&&r.variant==='before').text,records.find(r=>r.case===key&&r.run===run&&r.variant==='after').text);}
+ const quantile=(list,p)=>list.map(r=>r.readyMs).sort((a,b)=>a-b)[Math.ceil(list.length*p)-1];
+ const summary=Object.fromEntries(['before','after'].map(variant=>[variant,Object.fromEntries([['p50',.5],['p75',.75],['p95',.95]].map(([key,p])=>[key,quantile(records.filter(r=>r.variant===variant),p)]))]));
+ fs.writeFileSync(out+(journeys?'/mobile-journeys-smoke.json':'/mobile-browser-paired.json'),JSON.stringify({scope:journeys?'Isolated two-role five-route cold functional/visual parity; fixture data only; no production speed claim':'Isolated actual Angular production browser builds; 301 same-day tasks, 400 ms each fixture API; fresh browser/login/cache; gzip, 20 ms 4 Mbps; complete ordered list, same stats and enabled controls; not production real-user measurement',...(journeys?{}:{summary}),records:records.map(({text,...row})=>row)},null,2));if(!journeys)console.log(JSON.stringify(summary));
+}finally{await browser.close();for(const server of servers)await new Promise(r=>server.close(r));}

+ 50 - 0
perf/speed-round3-2026-11-10/mobile-frontend-report.md

@@ -0,0 +1,50 @@
+# 移动端前端验证
+
+本轮前端实现包含三处可验证的等待或重复读取削减:
+
+- 老师主页当天复习列表,首个分页返回后,剩余分页最多同时读取三页。全部成功后按页码恢复完整列表;仍保留原有日缓存、手动刷新、失败后完整重试、账号隔离和已确认完成状态。
+- 学生主页只显示账号余额,因此向 `app_account_overview` 发送 `profileOnly: true`。完整“我的”页面仍读取完整账号摘要;后端新参数的授权、资料等价和零额外统计查询由移动网关测试验证。
+- `app_lesson_study` 和 `app_reading_source` 都是服务器纯读取。前端原先把它们当成写入,成功后错误失效课表快照并跨标签广播;现在按读取处理。真实教学写入仍执行原来的失效与通知。
+
+## 范围与判定
+
+已检查学生和老师主页、伴学/陪练排课、账号、学习记录、复习日历、预约详情,以及共享 API、路由和 PDF 依赖。主页原有统计/余额/复习已经并发;主要页面按路由加载,PDF 已动态导入。老师学习记录先加载允许选择的学员,再读取选中学员记录,保留该权限和选择依赖。
+
+优化前构建及源码在首次修改前保存于 `/tmp/xiaoshu-mobile-perf-20261110/baseline-dist` 和 `baseline-source`;保存时 HEAD 为 `a47e560bfe7e298c7ecbfbb3ffac0f9e9e309451`。
+
+## 隔离浏览器配对测量
+
+这是生产 Angular 构建在匿名固定 API 夹具下的实验,不代表线上用户速度。两个版本使用同样 gzip、20 ms 延迟、4 Mbps 下行、3 Mbps 上行、每个分页 400 ms 固定接口耗时。每次使用新的浏览器上下文、登录态和缓存;AB/BA 交替各 10 次。
+
+老师主页夹具包含 301 条当天任务,共四页。终点是全部 301 条任务已经显示、任务与统计刷新按钮都可用;前后完整可见文本逐对一致。
+
+| 完整任务列表可用 | 优化前 | 优化后 |
+|---|---:|---:|
+| p50 | 2264 ms | 1456 ms |
+| p75 | 2295 ms | 1462 ms |
+| p95 | 2367 ms | 1504 ms |
+| 分页请求数 | 4 | 4 |
+| 同时执行的分页上限(夹具实际观察) | 1 | 3 |
+
+p75 降幅 36.3%。这证明消除了多页场景的串行网络等待;不足一页的当天任务不会从分页并发得到同样收益。真实数据库/API和最终生产验证应分别报告。
+
+`mobile-browser-paired.json` 保存匿名样本;`mobile-browser-paired.mjs` 的运行方式:
+
+```sh
+node perf/speed-round3-2026-11-10/mobile-browser-paired.mjs /tmp/xiaoshu-mobile-perf-20261110/baseline-dist/browser
+MOBILE_JOURNEYS=1 node perf/speed-round3-2026-11-10/mobile-browser-paired.mjs /tmp/xiaoshu-mobile-perf-20261110/baseline-dist/browser
+```
+
+第二条是两种角色各五条冷页面的功能/可见内容冒烟:主页、伴学/陪练、账号、学习记录、复习日历。它验证前后对应页面完整文本一致、夹具业务数据与可用控件存在、无页面运行错误,并保存 390 和 1024 宽截图。这组单次冒烟耗时不是速度结论。
+
+20 组截图中,18 组像素完全一致;两组学习记录窄屏截图仅卡片顶边一行 354 像素差异(0.108%),人工查看无内容或布局变化。截图在调整宽度后等待合成稳定再捕获,比较记录见 `mobile-visual-comparison.json`。
+
+## 检查
+
+- 改动前老师当天复习服务:7 项通过;新增并行顺序测试在旧实现上失败。
+- 老师复习服务与主页/学习模块路由集成:80 项通过。
+- API 分类新增测试在旧实现上出现预期的两项失败;修复后 API 与老师复习服务共 56 项通过,覆盖最大并发、乱序返回、服务端日期、失败重试、缓存隔离、读不失效和真实写入继续失效。
+- `npm run build:mobile` 通过;保留已有样式预算和 html2pdf CommonJS 提示。
+- `git diff --check` 通过。
+
+功能验证不替代真实账号、数据库、设备和上线验收。本文件只报告该前端范围,无部署或 Git 提交动作。

+ 221 - 0
perf/speed-round3-2026-11-10/mobile-journeys-smoke.json

@@ -0,0 +1,221 @@
+{
+  "scope": "Isolated two-role five-route cold functional/visual parity; fixture data only; no production speed claim",
+  "records": [
+    {
+      "case": "1-home",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 959,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_account_overview",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "1-home",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 946,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_account_overview",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "1-companion",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 623,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_companion_overview"
+      ]
+    },
+    {
+      "case": "1-companion",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 625,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_companion_overview"
+      ]
+    },
+    {
+      "case": "1-account",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 659,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_account_overview",
+        "app_support_requests_list"
+      ]
+    },
+    {
+      "case": "1-account",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 618,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_account_overview",
+        "app_support_requests_list"
+      ]
+    },
+    {
+      "case": "1-history",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 616,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_live_versions",
+        "app_learning_overview"
+      ]
+    },
+    {
+      "case": "1-history",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 606,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_live_versions",
+        "app_learning_overview"
+      ]
+    },
+    {
+      "case": "1-review",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 998,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "1-review",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 998,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 947,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-home",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 954,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "e_order_tongji",
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-companion",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 630,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_companion_overview"
+      ]
+    },
+    {
+      "case": "3-companion",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 611,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_companion_overview"
+      ]
+    },
+    {
+      "case": "3-account",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 560,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_account_overview",
+        "app_support_requests_list"
+      ]
+    },
+    {
+      "case": "3-account",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 561,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "app_account_overview",
+        "app_support_requests_list"
+      ]
+    },
+    {
+      "case": "3-history",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 644,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "e_user_list",
+        "app_live_versions",
+        "app_learning_overview"
+      ]
+    },
+    {
+      "case": "3-history",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 651,
+      "maxConcurrentReviewReads": 0,
+      "actions": [
+        "e_user_list",
+        "app_live_versions",
+        "app_learning_overview"
+      ]
+    },
+    {
+      "case": "3-review",
+      "run": 0,
+      "variant": "before",
+      "readyMs": 1010,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "app_review_page"
+      ]
+    },
+    {
+      "case": "3-review",
+      "run": 0,
+      "variant": "after",
+      "readyMs": 994,
+      "maxConcurrentReviewReads": 1,
+      "actions": [
+        "app_review_page"
+      ]
+    }
+  ]
+}

+ 50 - 0
perf/speed-round3-2026-11-10/mobile-profile-summary.json

@@ -0,0 +1,50 @@
+{
+  "context": "Operator-bound read-only projections of existing real role records; not role-login acceptance.",
+  "results": [
+    {
+      "case": "companion",
+      "variant": "baseline",
+      "durationMs": 73,
+      "queryCount": 5,
+      "queryDurations": [
+        16,
+        15,
+        12,
+        14,
+        14
+      ],
+      "rows": 1,
+      "total": 1
+    },
+    {
+      "case": "review",
+      "variant": "baseline",
+      "durationMs": 1625,
+      "queryCount": 5,
+      "queryDurations": [
+        299,
+        499,
+        1216,
+        1549,
+        1622
+      ],
+      "rows": 4,
+      "total": 4
+    },
+    {
+      "case": "companion",
+      "variant": "candidate",
+      "durationMs": 56,
+      "queryCount": 5,
+      "queryDurations": [
+        22,
+        30,
+        30,
+        19,
+        13
+      ],
+      "rows": 1,
+      "total": 1
+    }
+  ]
+}

+ 36 - 0
perf/speed-round3-2026-11-10/mobile-profile.mjs

@@ -0,0 +1,36 @@
+import fs from 'node:fs';
+import ts from 'typescript';
+import {withGateway} from './server-util.mjs';
+const privateDir='tmp/speed-round3-2026-11-10';fs.mkdirSync(privateDir,{recursive:true});
+const baseline=JSON.parse(fs.readFileSync('/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3/mobile-gateway.json','utf8')).code;
+const local=fs.readFileSync('scripts/deploy-admin-functions.mjs','utf8').match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
+const parsed=ts.createSourceFile('gateway.js',local,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);
+function helper(name){const node=parsed.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name?.text===name);if(!node)throw Error('Missing helper '+name);return node.getText(parsed).replace('function '+name+'(','function candidate'+name+'(');}
+const company=baseline.match(/const DEFAULT_COMPANY_ID\s*=\s*['"]([^'"]+)/)?.[1];if(!company)throw Error('Missing tenant');
+const subjectSql=`WITH counts AS (SELECT pl AS uid,COUNT(*)::int appointments FROM "CourseAppointment" WHERE company=$1 AND pl<>'' GROUP BY pl) SELECT u."objectId",u."legacyUserId",u."legacyGroupId",c.appointments,(SELECT COUNT(*)::int FROM "MemoryPracticeRecord" m WHERE m.company=u.company AND m.plid=u."legacyUserId") AS reviews FROM counts c JOIN "_User" u ON u.company=$1 AND u."legacyUserId"::text=c.uid WHERE u."legacyGroupId"=3 AND COALESCE(u."isDeleted",false)=false ORDER BY c.appointments DESC LIMIT 2`;
+const subjects=await withGateway(`async function run(){return await Psql.query(${JSON.stringify(subjectSql)},[${JSON.stringify(company)}]);}`,async call=>call({}));
+fs.writeFileSync(privateDir+'/mobile-subjects.json',JSON.stringify(subjects,null,2));
+console.log(JSON.stringify({subjects:subjects.map((s,i)=>({label:'teacher-'+(i+1),appointments:s.appointments,reviews:s.reviews}))}));
+if(!subjects.length)throw Error('No teacher subjects');
+const instrumentation=`let mobileSqlCalls=[];
+const MOBILE_PROFILE_DB=new Proxy({},{get(_target,name){const target=Psql;if(!['query','one','oneOrNone'].includes(name))return target[name];return async(sql,args)=>{const started=Date.now();try{const value=await target[name](sql,args);mobileSqlCalls.push({method:name,sql,args,durationMs:Date.now()-started,rows:Array.isArray(value)?value.length:1});return value;}catch(error){mobileSqlCalls.push({method:name,sql,args,durationMs:Date.now()-started,error:error.message});throw error;}};}});`;
+const source=instrumentation+'\n'+(baseline+'\n'+helper('appCompanionOverview')+'\n'+helper('appAccountOverview')).replace(/\bPsql\b/g,'MOBILE_PROFILE_DB')+`
+async function run(input){
+ const subjects=${JSON.stringify(subjects.map(s=>s.objectId))};
+ const selected=Number(input.subject)||0;if(selected<0||selected>=subjects.length)throw Error('Unknown subject');
+ const current=await new Parse.Query('_User').get(subjects[selected],{useMasterKey:true});
+ const cases={companion:{source:'database',page:1,pageSize:20,dateStart:'2026-11-10',dateEnd:'2026-11-10'},history:{source:'database',history:true,month:'2026-11',page:1,pageSize:20},review:{scope:'today',page:1,pageSize:100,includeMetadata:false,includeIncome:false},profile:{source:'database',profileOnly:true}};
+ const params=cases[input.case];if(!params)throw Error('Unknown case');
+ const names=input.variant==='candidate'?{companion:candidateappCompanionOverview,history:candidateappCompanionOverview,review:appReviewPage,profile:candidateappAccountOverview}:{companion:appCompanionOverview,history:appCompanionOverview,review:appReviewPage,profile:appAccountOverview};
+ mobileSqlCalls=[];const started=Date.now();const result=await names[input.case](params,current);return {durationMs:Date.now()-started,calls:mobileSqlCalls,result};
+}`;
+const results=[];await withGateway(source,async call=>{
+ for(const caseName of ['companion','review']){
+  const value=await call({case:caseName,variant:'baseline',subject:0});
+  fs.writeFileSync(privateDir+'/mobile-'+caseName+'-baseline.json',JSON.stringify(value,null,2));
+  const summary={case:caseName,variant:'baseline',durationMs:value.durationMs,queryCount:value.calls.length,queryDurations:value.calls.map(c=>c.durationMs),rows:value.result.schedules?.length??value.result.items?.length,total:value.result.page?.itemCount};results.push(summary);console.log(JSON.stringify(summary));
+ }
+ const value=await call({case:'companion',variant:'candidate',subject:0});fs.writeFileSync(privateDir+'/mobile-companion-candidate.json',JSON.stringify(value,null,2));
+ const summary={case:'companion',variant:'candidate',durationMs:value.durationMs,queryCount:value.calls.length,queryDurations:value.calls.map(c=>c.durationMs),rows:value.result.schedules?.length,total:value.result.page?.itemCount};results.push(summary);console.log(JSON.stringify(summary));
+});
+fs.writeFileSync('perf/speed-round3-2026-11-10/mobile-profile-summary.json',JSON.stringify({context:'Operator-bound read-only projections of existing real role records; not role-login acceptance.',results},null,2));

+ 52 - 0
perf/speed-round3-2026-11-10/mobile-report.md

@@ -0,0 +1,52 @@
+# Mobile cold-read optimization, 2026-11-10
+
+The candidate performs fresh SQL on every request. It introduces no result cache and requires no database migration or persistent index change.
+
+## Paired real-data API measurements
+
+Ten sequential alternating baseline/candidate samples per case, using the saved production gateway and four selectively substituted candidate helpers in an operator-bound temporary diagnostic function. An existing teacher has 1,776 appointments and 3,321 review tasks. The student case uses an existing student with populated November appointments. Names, identifiers, SQL arguments and response bodies remain in the ignored private `tmp/speed-round3-2026-11-10/` directory.
+
+| Case | Baseline service p75 | Candidate service p75 | Reduction | Baseline request p75 | Candidate request p75 |
+| --- | ---: | ---: | ---: | ---: | ---: |
+| Teacher current-day appointments | 82 ms | 50 ms | 39.0% | 234 ms | 164 ms |
+| Teacher current-day reviews | 1,701 ms | 62 ms | 96.4% | 1,819 ms | 246 ms |
+| Student November appointments | 49 ms | 32 ms | 34.7% | 211 ms | 204 ms |
+| Student profile/balances | 100 ms | 1 ms | 99.0% | 223 ms | 157 ms |
+
+Service time starts after fetching the real user's current Parse record; request time also includes that fetch, transport and diagnostic dispatch. Millisecond timer resolution means the 1 ms profile measurement should be read as negligible helper work, not a 1 ms page-load claim. Request noise limits the student appointment benefit visible over the network.
+
+All 40 pairs preserve business outputs (the profile case compares the profile consumed by the homepage). Only refresh/server timestamps and lagSeconds are excluded from equality. Appointments and reviews continue to execute five SQL queries each; the profile-only path executes zero instead of five.
+
+These are operator-authorized read-only projections through actual stored student/teacher roles and helper authorization, **not real-role login or browser acceptance**. Student account legacy synchronization is replaced with a no-write stub in both variants, so its baseline excludes legacy synchronization cost. The production helper does not receive that stub. No users, tokens or business rows were created or changed; each temporary Function is bound to the existing operator, expires after one hour and is deleted in `finally`.
+
+## Cause and exact changes
+
+- `appReviewPage` previously allowed the planner to precompute its empty-study exclusion against all 3,173 empty studies in the tenant. Each of the summary, count and rows queries performed about 62,974 CommonModel lookups in this source check. A zero-offset correlation barrier preserves the identical NOT EXISTS predicate while looking up the current task's study first.
+- The original numeric-owner-to-text comparison remains intact. A redundant numeric comparison permits the existing ownership indexes to narrow candidates. It deliberately retains exact text-format semantics, including numeric scale, instead of broadening access.
+- The review rows' CommonModel joins use equivalent correlated lateral lookups with zero offsets to prevent scanning 205,005 review models or 20,122 study models before finding the requested task. Inner/left join, duplicate and missing-row behavior remains unchanged.
+- `appLiveScopeVersion` uses the same redundant indexed ownership conditions for the review scope, preserving the exact original text matches, timestamps and counts.
+- `appCompanionOverview` retains its SQL and parameters, runs at most three independent SQL reads concurrently, waits for in-flight reads to settle on failure, then calculates historical income once and reads versions last.
+- `appAccountOverview` adds opt-in `profileOnly:true` after authorization and fresh profile construction. Its default full response is unchanged. The homepage consumes this limited response through the frontend change.
+
+Exploratory EXPLAIN ANALYZE showed review summary/list/count at approximately 921/868/856 ms before and 39/63/2 ms after; review version changed from 74 ms to 2 ms. These exploratory timings identified the plan changes; the paired measurements above are the API comparison. Plans and raw payloads remain private; sanitized baseline plan counts are in `mobile-review-plan-summary.json`.
+
+## Validation and selective release
+
+The release must replace exactly these mobile gateway functions from the saved live baseline:
+
+1. `appCompanionOverview` — `scripts/deploy-admin-functions.mjs`
+2. `appAccountOverview` — `scripts/deploy-admin-functions.mjs`
+3. `appLiveScopeVersion` — `scripts/deploy-admin-functions.mjs`
+4. `appReviewPage` — `scripts/cloud/mobile-teaching.js`
+
+Both original companion/account functions and the original review function were compared with the saved live definition before editing and matched exactly. Golden tests cover both roles, pagination, pending/completed/day/month filters, metadata/income flags, corrected rewards, missing models, foreign tenants and numeric text-format identity. Existing writing-source, assignment-access, teacher income and mobile teaching tests cover preserved business behavior.
+
+Validation result: 29 focused tests passed across mobile read/review performance, teacher earnings, legacy writing review, assignment access, student display names and mobile teaching; `npm run cloud:validate` validated 11 cloud functions. `git diff --check` passed.
+
+Measurement command:
+
+```sh
+node --import ./tmp/admin-speed-round2-2026-11-10/env.mjs perf/speed-round3-2026-11-10/mobile-benchmark.mjs
+```
+
+The candidate has been measured through a temporary gateway only. Main gateway deployment, real browser flows and final Git synchronization belong to the parent release task.

+ 7 - 0
perf/speed-round3-2026-11-10/mobile-review-correlation.mjs

@@ -0,0 +1,7 @@
+import fs from 'node:fs';import vm from 'node:vm';import {withGateway} from './server-util.mjs';
+const ctx=vm.createContext({});vm.runInContext(fs.readFileSync('scripts/cloud/mobile-teaching.js','utf8'),ctx);const notes=ctx.notesReviewSourceSql();
+const captured=JSON.parse(fs.readFileSync('tmp/speed-round3-2026-11-10/mobile-review-baseline.json','utf8')).calls;
+const calls=captured.filter(c=>c.sql.includes('AND NOT ('+notes+'))')).map(c=>({...c,candidate:c.sql.replace('AND NOT ('+notes+'))','AND NOT ('+notes+') OFFSET 0)').replace('m."plid"::text=$2','(m."plid"=$2::text::numeric AND m."plid"::text=$2)').replace('JOIN "CommonModel" c ON c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id"','JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" OFFSET 0) c ON TRUE').replace('LEFT JOIN "CommonModel" dc ON dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text','LEFT JOIN LATERAL (SELECT * FROM "CommonModel" dc WHERE dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text OFFSET 0) dc ON TRUE')}));if(calls.length!==3)throw Error('Expected three correlated review reads');
+const source=`async function run(input){const calls=${JSON.stringify(calls)};const c=calls[Number(input.index)];if(!c)throw Error('Unknown fixed query');const before=await Psql.query(c.sql,c.args),after=await Psql.query(c.candidate,c.args),plan=await Psql.query('EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) '+c.candidate,c.args);return{equal:JSON.stringify(before)===JSON.stringify(after),rows:after.length,plan};}`;
+const results=[];await withGateway(source,async call=>{for(let i=0;i<calls.length;i++){const value=await call({index:i});results.push(value);console.log(JSON.stringify({query:i,equal:value.equal,rows:value.rows,executionMs:value.plan[0]['QUERY PLAN'][0]['Execution Time']}));}});
+fs.writeFileSync('tmp/speed-round3-2026-11-10/mobile-review-correlation-lateral.json',JSON.stringify(results,null,2));

+ 549 - 0
perf/speed-round3-2026-11-10/mobile-review-plan-summary.json

@@ -0,0 +1,549 @@
+[
+  {
+    "query": 0,
+    "executionMs": 74.01,
+    "nodes": [
+      {
+        "node": "Aggregate",
+        "rows": 1,
+        "loops": 1,
+        "time": 73.978
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "MemoryPracticeRecord",
+        "rows": 3321,
+        "loops": 1,
+        "time": 73.722,
+        "removed": 201684
+      }
+    ]
+  },
+  {
+    "query": 1,
+    "executionMs": 921.467,
+    "nodes": [
+      {
+        "node": "Aggregate",
+        "rows": 1,
+        "loops": 1,
+        "time": 921.264
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 3321,
+        "loops": 1,
+        "time": 918.454
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "MemoryPracticeRecord",
+        "rows": 3321,
+        "loops": 1,
+        "time": 53.306,
+        "removed": 201684
+      },
+      {
+        "node": "Materialize",
+        "rows": 18,
+        "loops": 3321,
+        "time": 0.256
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 18,
+        "loops": 1,
+        "time": 845.897
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 18,
+        "loops": 1,
+        "time": 845.673
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "DailyStudyRecord",
+        "rows": 3173,
+        "loops": 1,
+        "time": 13.6,
+        "removed": 16949
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 1,
+        "loops": 3173,
+        "time": 0.262
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CourseAppointment",
+        "index": "idx_appt_company_student_effective_date",
+        "rows": 20,
+        "loops": 3173,
+        "time": 0.088,
+        "removed": 51
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "idx_cm_company_model_item",
+        "rows": 1,
+        "loops": 62974,
+        "time": 0.006,
+        "removed": 0
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 1,
+        "loops": 94,
+        "time": 1.462
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "LessonRecord",
+        "rows": 1,
+        "loops": 94,
+        "time": 1.44,
+        "removed": 3826
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 94,
+        "time": 0.016,
+        "removed": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 18,
+        "time": 0.01
+      }
+    ]
+  },
+  {
+    "query": 2,
+    "executionMs": 867.935,
+    "nodes": [
+      {
+        "node": "Limit",
+        "rows": 4,
+        "loops": 1,
+        "time": 867.217
+      },
+      {
+        "node": "Result",
+        "rows": 4,
+        "loops": 1,
+        "time": 867.214
+      },
+      {
+        "node": "Sort",
+        "rows": 4,
+        "loops": 1,
+        "time": 867.1
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 867.055
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 862.999
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 862.933
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 69.75
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 69.696
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 69.635
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 68.705
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "MemoryPracticeRecord",
+        "rows": 4,
+        "loops": 1,
+        "time": 68.616,
+        "removed": 205001
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 4,
+        "time": 0.018
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "Node",
+        "rows": 463,
+        "loops": 4,
+        "time": 0.103,
+        "removed": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_general_text",
+        "rows": 1,
+        "loops": 4,
+        "time": 0.012
+      },
+      {
+        "node": "Index Scan",
+        "relation": "DailyStudyRecord",
+        "index": "idx_daily_company_id",
+        "rows": 1,
+        "loops": 4,
+        "time": 0.01
+      },
+      {
+        "node": "Materialize",
+        "rows": 18,
+        "loops": 4,
+        "time": 198.28
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 18,
+        "loops": 1,
+        "time": 793.066
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 18,
+        "loops": 1,
+        "time": 792.828
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "DailyStudyRecord",
+        "rows": 3173,
+        "loops": 1,
+        "time": 13.425,
+        "removed": 16949
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 1,
+        "loops": 3173,
+        "time": 0.245
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CourseAppointment",
+        "index": "idx_appt_company_student_effective_date",
+        "rows": 20,
+        "loops": 3173,
+        "time": 0.083,
+        "removed": 51
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "idx_cm_company_model_item",
+        "rows": 1,
+        "loops": 62974,
+        "time": 0.006,
+        "removed": 0
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 1,
+        "loops": 94,
+        "time": 1.397
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "LessonRecord",
+        "rows": 1,
+        "loops": 94,
+        "time": 1.379,
+        "removed": 3826
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 94,
+        "time": 0.011,
+        "removed": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 18,
+        "time": 0.011
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 0,
+        "loops": 4,
+        "time": 0.014
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 0,
+        "loops": 4,
+        "time": 0.013
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 0,
+        "loops": 4,
+        "time": 0.012
+      },
+      {
+        "node": "Index Scan",
+        "relation": "ReviewReward",
+        "index": "ReviewReward_company_taskId_key",
+        "rows": 0,
+        "loops": 4,
+        "time": 0.011
+      },
+      {
+        "node": "Index Scan",
+        "relation": "ReviewRewardRateCorrection",
+        "index": "ReviewRewardRateCorrection_company_rewardId_key",
+        "rows": 0,
+        "loops": 0,
+        "time": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "ReviewRewardResolution",
+        "index": "ReviewRewardResolution_company_rewardId_key",
+        "rows": 0,
+        "loops": 0,
+        "time": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "ReviewRewardCorrection",
+        "index": "ReviewRewardCorrection_company_rewardId_key",
+        "rows": 0,
+        "loops": 0,
+        "time": 0
+      },
+      {
+        "node": "Materialize",
+        "rows": 1235,
+        "loops": 4,
+        "time": 0.659
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "_User",
+        "rows": 1235,
+        "loops": 1,
+        "time": 1.199,
+        "removed": 1
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 0,
+        "loops": 0,
+        "time": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CourseAppointment",
+        "index": "idx_appt_company_student_effective_date",
+        "rows": 0,
+        "loops": 0,
+        "time": 0,
+        "removed": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 0,
+        "loops": 0,
+        "time": 0,
+        "removed": 0
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 0,
+        "loops": 0,
+        "time": 0
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "LessonRecord",
+        "rows": 0,
+        "loops": 0,
+        "time": 0,
+        "removed": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 0,
+        "loops": 0,
+        "time": 0,
+        "removed": 0
+      },
+      {
+        "node": "Aggregate",
+        "rows": 1,
+        "loops": 4,
+        "time": 0.022
+      },
+      {
+        "node": "Index Scan",
+        "relation": "MemoryPracticeRecord",
+        "index": "memory_review_source_day",
+        "rows": 7,
+        "loops": 4,
+        "time": 0.02,
+        "removed": 3
+      }
+    ]
+  },
+  {
+    "query": 3,
+    "executionMs": 855.91,
+    "nodes": [
+      {
+        "node": "Aggregate",
+        "rows": 1,
+        "loops": 1,
+        "time": 855.744
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 4,
+        "loops": 1,
+        "time": 855.736
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "MemoryPracticeRecord",
+        "rows": 4,
+        "loops": 1,
+        "time": 60.516,
+        "removed": 205001
+      },
+      {
+        "node": "Materialize",
+        "rows": 18,
+        "loops": 4,
+        "time": 198.792
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 18,
+        "loops": 1,
+        "time": 795.131
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 18,
+        "loops": 1,
+        "time": 794.911
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "DailyStudyRecord",
+        "rows": 3173,
+        "loops": 1,
+        "time": 13.325,
+        "removed": 16949
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 1,
+        "loops": 3173,
+        "time": 0.246
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CourseAppointment",
+        "index": "idx_appt_company_student_effective_date",
+        "rows": 20,
+        "loops": 3173,
+        "time": 0.083,
+        "removed": 51
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "idx_cm_company_model_item",
+        "rows": 1,
+        "loops": 62974,
+        "time": 0.005,
+        "removed": 0
+      },
+      {
+        "node": "Nested Loop",
+        "rows": 1,
+        "loops": 94,
+        "time": 1.475
+      },
+      {
+        "node": "Seq Scan",
+        "relation": "LessonRecord",
+        "rows": 1,
+        "loops": 94,
+        "time": 1.46,
+        "removed": 3826
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 94,
+        "time": 0.009,
+        "removed": 0
+      },
+      {
+        "node": "Index Scan",
+        "relation": "CommonModel",
+        "index": "payroll_common_item",
+        "rows": 1,
+        "loops": 18,
+        "time": 0.01
+      }
+    ]
+  }
+]

+ 11 - 0
perf/speed-round3-2026-11-10/mobile-review-plan.mjs

@@ -0,0 +1,11 @@
+import fs from 'node:fs';
+import {withGateway} from './server-util.mjs';
+const data=JSON.parse(fs.readFileSync('tmp/speed-round3-2026-11-10/mobile-review-baseline.json','utf8'));
+const selected=data.calls.filter(c=>c.sql.includes('FROM "MemoryPracticeRecord"'));
+const source=`async function run(input){const calls=${JSON.stringify(selected)};if(input.metadata)return {indexes:await Psql.query('SELECT tablename,indexname,indexdef FROM pg_indexes WHERE schemaname=current_schema() AND tablename=ANY($1::text[])',[['MemoryPracticeRecord','CommonModel','DailyStudyRecord']]),types:await Psql.query('SELECT table_name,column_name,data_type FROM information_schema.columns WHERE table_schema=current_schema() AND table_name=ANY($1::text[]) AND column_name=ANY($2::text[])',[['MemoryPracticeRecord','CommonModel','DailyStudyRecord'],['id','plid','yhid','xxjlid','generalId','itemId']])};const item=calls[Number(input.index)];if(!item)throw Error('Invalid fixed query');return await Psql.query('EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) '+item.sql,item.args);}`;
+const plans=[];await withGateway(source,async call=>{
+ fs.writeFileSync('tmp/speed-round3-2026-11-10/mobile-review-indexes.json',JSON.stringify(await call({metadata:true}),null,2));
+ for(let i=0;i<selected.length;i++){const value=await call({index:i});plans.push({sql:selected[i].sql,plan:value});const plan=value[0]['QUERY PLAN'][0];console.log(JSON.stringify({index:i,executionMs:plan['Execution Time'],planningMs:plan['Planning Time']}));}
+});fs.writeFileSync('tmp/speed-round3-2026-11-10/mobile-review-plans.json',JSON.stringify(plans,null,2));
+function flatten(node,out=[]){out.push({node:node['Node Type'],relation:node['Relation Name'],index:node['Index Name'],rows:node['Actual Rows'],loops:node['Actual Loops'],time:node['Actual Total Time'],removed:node['Rows Removed by Filter']});for(const child of node.Plans||[])flatten(child,out);return out;}
+fs.writeFileSync('perf/speed-round3-2026-11-10/mobile-review-plan-summary.json',JSON.stringify(plans.map((p,i)=>({query:i,executionMs:p.plan[0]['QUERY PLAN'][0]['Execution Time'],nodes:flatten(p.plan[0]['QUERY PLAN'][0].Plan)})),null,2));

+ 5 - 0
perf/speed-round3-2026-11-10/mobile-review-version.mjs

@@ -0,0 +1,5 @@
+import fs from 'node:fs';import {withGateway} from './server-util.mjs';
+const before=JSON.parse(fs.readFileSync('tmp/speed-round3-2026-11-10/mobile-review-baseline.json','utf8')).calls.find(c=>c.sql.startsWith('SELECT MAX(COALESCE')&&c.sql.includes('FROM "MemoryPracticeRecord"'));
+const after=before.sql.replace('CAST("yhid" AS text)=$2','("yhid"=$2::text::numeric AND CAST("yhid" AS text)=$2)').replace('CAST("plid" AS text)=$2','("plid"=$2::text::numeric AND CAST("plid" AS text)=$2)');
+const source=`async function run(){const args=${JSON.stringify(before.args)},before=await Psql.query(${JSON.stringify(before.sql)},args),after=await Psql.query(${JSON.stringify(after)},args),plan=await Psql.query('EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) '+${JSON.stringify(after)},args);return{equal:JSON.stringify(before)===JSON.stringify(after),plan};}`;
+const result=await withGateway(source,async call=>call({}));fs.writeFileSync('tmp/speed-round3-2026-11-10/mobile-review-version.json',JSON.stringify(result,null,2));console.log(JSON.stringify({equal:result.equal,executionMs:result.plan[0]['QUERY PLAN'][0]['Execution Time']}));

+ 122 - 0
perf/speed-round3-2026-11-10/mobile-visual-comparison.json

@@ -0,0 +1,122 @@
+[
+  {
+    "before": "mobile-1-account-before-1024.png",
+    "after": "mobile-1-account-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-1-account-before-390.png",
+    "after": "mobile-1-account-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-1-companion-before-1024.png",
+    "after": "mobile-1-companion-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-1-companion-before-390.png",
+    "after": "mobile-1-companion-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-1-history-before-1024.png",
+    "after": "mobile-1-history-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-1-history-before-390.png",
+    "after": "mobile-1-history-after-390.png",
+    "changedPixels": 354,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-1-home-before-1024.png",
+    "after": "mobile-1-home-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-1-home-before-390.png",
+    "after": "mobile-1-home-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-1-review-before-1024.png",
+    "after": "mobile-1-review-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-1-review-before-390.png",
+    "after": "mobile-1-review-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-3-account-before-1024.png",
+    "after": "mobile-3-account-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-3-account-before-390.png",
+    "after": "mobile-3-account-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-3-companion-before-1024.png",
+    "after": "mobile-3-companion-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-3-companion-before-390.png",
+    "after": "mobile-3-companion-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-3-history-before-1024.png",
+    "after": "mobile-3-history-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-3-history-before-390.png",
+    "after": "mobile-3-history-after-390.png",
+    "changedPixels": 354,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-3-home-before-1024.png",
+    "after": "mobile-3-home-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-3-home-before-390.png",
+    "after": "mobile-3-home-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  },
+  {
+    "before": "mobile-3-review-before-1024.png",
+    "after": "mobile-3-review-after-1024.png",
+    "changedPixels": 0,
+    "totalPixels": 864256
+  },
+  {
+    "before": "mobile-3-review-before-390.png",
+    "after": "mobile-3-review-after-390.png",
+    "changedPixels": 0,
+    "totalPixels": 329160
+  }
+]

+ 18 - 0
perf/speed-round3-2026-11-10/month-ledger-baseline.json

@@ -0,0 +1,18 @@
+{
+  "batches": [
+    {
+      "month": "2026-08",
+      "status": "draft",
+      "batches": 1
+    }
+  ],
+  "lines": [
+    {
+      "month": "2026-08",
+      "status": "draft",
+      "kind": "lesson",
+      "lines": 892,
+      "coaches": 29
+    }
+  ]
+}

+ 225 - 0
perf/speed-round3-2026-11-10/payroll-api-isolated-smoke.json

@@ -0,0 +1,225 @@
+{
+  "context": "Cold round-2 production admin gateway baseline versus selective round-3 candidate on one static real-data clone. Ten sequential pairs alternate AB/BA. Each SQL operation uses clone-only search_path and RR; explicit candidate transactions preserve their single-snapshot semantics. TeachingCourseCategory and CourseTeachingProfile Parse reads/schema lookups are prefetched and shared within each pair OUTSIDE service timers. Service time also excludes setup, cache clearing, authentication and transport; pairRequestMs includes those per-pair prefetches/clears and transport for BOTH variants. This is not full API user-experience timing. ReviewRewardIncome is frozen as an equivalent copied-row view. All clone result caches are emptied before and after every variant. Digest readiness is stubbed for an unchanged copied digest: no source-write/trigger acceptance is claimed. Operator-bound manager projection, not manager-login acceptance.",
+  "month": "2026-11",
+  "baselineHash": "5cb36e2e4c4fa3ad8fb342b80fa2ea4acf6dc33ba91ef53d036151708472d310",
+  "candidateHash": "959369b4261294ed62d263aeb89afc2e9d56adb82d43c60e7b2f1847da0071ac",
+  "setup": {
+    "guards": {
+      "authority": true,
+      "digest": true,
+      "source_index": true,
+      "snapshot": true
+    },
+    "copiedTables": 33,
+    "staticDigestOnly": true
+  },
+  "removed": true,
+  "fullOutputEquality": true,
+  "normalization": [
+    "refreshedAt",
+    "lagSeconds",
+    "cacheHit"
+  ],
+  "summary": {
+    "baseline": {
+      "samples": 1,
+      "p50Ms": 6985,
+      "p75Ms": 6985,
+      "p95Ms": 6985
+    },
+    "candidate": {
+      "samples": 1,
+      "p50Ms": 3273,
+      "p75Ms": 3273,
+      "p95Ms": 3273
+    }
+  },
+  "samples": [
+    {
+      "round": 0,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 6985,
+      "pairRequestMs": 11200,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 223,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 208,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 215,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 228,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 294,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 319,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3453,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5572,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 77,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 135,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 312,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 7,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 13,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 0,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 3273,
+      "pairRequestMs": 11200,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 153,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 6,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 6,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 14,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 39,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 56,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2736,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 29,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 72,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 222,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    }
+  ]
+}

+ 1899 - 0
perf/speed-round3-2026-11-10/payroll-api-isolated.json

@@ -0,0 +1,1899 @@
+{
+  "context": "Cold round-2 production admin gateway baseline versus selective round-3 candidate on one static real-data clone. Ten sequential pairs alternate AB/BA. Each SQL operation uses clone-only search_path and RR; explicit candidate transactions preserve their single-snapshot semantics. TeachingCourseCategory and CourseTeachingProfile Parse reads/schema lookups are prefetched and shared within each pair OUTSIDE service timers. Service time also excludes setup, cache clearing, authentication and transport; pairRequestMs includes those per-pair prefetches/clears and transport for BOTH variants. This is not full API user-experience timing. ReviewRewardIncome is frozen as an equivalent copied-row view. All clone result caches are emptied before and after every variant. Digest readiness is stubbed for an unchanged copied digest: no source-write/trigger acceptance is claimed. Operator-bound manager projection, not manager-login acceptance.",
+  "month": "2026-11",
+  "baselineHash": "5cb36e2e4c4fa3ad8fb342b80fa2ea4acf6dc33ba91ef53d036151708472d310",
+  "candidateHash": "959369b4261294ed62d263aeb89afc2e9d56adb82d43c60e7b2f1847da0071ac",
+  "setup": {
+    "guards": {
+      "authority": true,
+      "digest": true,
+      "source_index": true,
+      "snapshot": true
+    },
+    "copiedTables": 33,
+    "staticDigestOnly": true
+  },
+  "removed": true,
+  "fullOutputEquality": true,
+  "normalization": [
+    "refreshedAt",
+    "lagSeconds",
+    "cacheHit"
+  ],
+  "summary": {
+    "baseline": {
+      "samples": 10,
+      "p50Ms": 6135,
+      "p75Ms": 6835,
+      "p95Ms": 7274
+    },
+    "candidate": {
+      "samples": 10,
+      "p50Ms": 3009,
+      "p75Ms": 3257,
+      "p95Ms": 3623
+    }
+  },
+  "samples": [
+    {
+      "round": 0,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 7274,
+      "pairRequestMs": 11672,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 201,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 195,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 199,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 214,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 303,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 308,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 4256,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5874,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 97,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 204,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 239,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 10,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 0,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 3333,
+      "pairRequestMs": 11672,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 152,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 6,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 13,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 16,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 40,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 41,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "one",
+          "ms": 2793,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 28,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 70,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 222,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 1,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 3623,
+      "pairRequestMs": 10757,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 173,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 14,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 22,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 34,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 46,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3065,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 28,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 72,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 235,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 1,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 6492,
+      "pairRequestMs": 10757,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 178,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 41,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 44,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 53,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 112,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 118,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3318,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5333,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 81,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 72,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 222,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 2,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 7031,
+      "pairRequestMs": 10894,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 164,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 5,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 18,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 30,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 85,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 98,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3907,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5867,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 68,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 76,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 223,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 2,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 3257,
+      "pairRequestMs": 10894,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 151,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 5,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 15,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 19,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 69,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 87,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2705,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 28,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 75,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 230,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 3,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 2949,
+      "pairRequestMs": 10189,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 143,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 12,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 12,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 22,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 27,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 42,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2378,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 33,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 99,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 228,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 12,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 3,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 6692,
+      "pairRequestMs": 10189,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 157,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 38,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 41,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 64,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 95,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 97,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3687,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5502,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 86,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 79,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 214,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 4,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 6135,
+      "pairRequestMs": 10114,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 145,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 6,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 17,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 28,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 58,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 64,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3397,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5001,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 83,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 67,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 238,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 4,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 3009,
+      "pairRequestMs": 10114,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 150,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 5,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 17,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 35,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 45,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2487,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 27,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 72,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 212,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 5,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 2939,
+      "pairRequestMs": 9477,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 148,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 12,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 14,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 21,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 43,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 49,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2415,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 32,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 64,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 214,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 5,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 5861,
+      "pairRequestMs": 9477,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 176,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 34,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 40,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 53,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 94,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 117,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3457,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 4735,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 68,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 80,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 233,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 6,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 6064,
+      "pairRequestMs": 9475,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 151,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 7,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 17,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 29,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 92,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 120,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3561,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 4995,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 66,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 70,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 214,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 6,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 2871,
+      "pairRequestMs": 9475,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 145,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 5,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 18,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 28,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 41,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2363,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 28,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 72,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 214,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 7,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 3012,
+      "pairRequestMs": 9275,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 150,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 13,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 13,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 23,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 41,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 58,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2485,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 33,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 75,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 207,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 7,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 5780,
+      "pairRequestMs": 9275,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 146,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 46,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 52,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 67,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 92,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 97,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3272,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 4711,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 66,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 70,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 209,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 8,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 5993,
+      "pairRequestMs": 9624,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 153,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 5,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 13,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 25,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 52,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 103,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 3578,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 4887,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 72,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 73,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 230,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 8,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 2983,
+      "pairRequestMs": 9624,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 151,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 6,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 12,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 16,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 42,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 52,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2456,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 27,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 74,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 211,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 11,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 9,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 3020,
+      "pairRequestMs": 10394,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 150,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 16,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 17,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 27,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 63,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 69,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 2473,
+          "kind": "coherent-snapshot",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 26,
+          "kind": "thin-users",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 77,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 215,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 8,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 8,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    },
+    {
+      "round": 9,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 6835,
+      "pairRequestMs": 10394,
+      "outputHash": "e20e6a3a25f95365cad4cc63ac6f2736ead26246171ab98b9380b3b361dd944e",
+      "coldBefore": 0,
+      "cacheHit": false,
+      "queries": [
+        {
+          "method": "one",
+          "ms": 188,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 48,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 57,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "query",
+          "ms": 67,
+          "kind": "other",
+          "rows": 892
+        },
+        {
+          "method": "query",
+          "ms": 127,
+          "kind": "other",
+          "rows": 42
+        },
+        {
+          "method": "query",
+          "ms": 149,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 4539,
+          "kind": "sources-cold",
+          "rows": 1
+        },
+        {
+          "method": "one",
+          "ms": 5706,
+          "kind": "cutoff-cold",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 67,
+          "kind": "other",
+          "rows": 1235
+        },
+        {
+          "method": "query",
+          "ms": 73,
+          "kind": "other",
+          "rows": 1313
+        },
+        {
+          "method": "one",
+          "ms": 223,
+          "kind": "preview-cache",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 9,
+          "kind": "other",
+          "rows": 0
+        },
+        {
+          "method": "one",
+          "ms": 9,
+          "kind": "other",
+          "rows": 1
+        },
+        {
+          "method": "query",
+          "ms": 10,
+          "kind": "other",
+          "rows": 9
+        }
+      ]
+    }
+  ]
+}

+ 112 - 0
perf/speed-round3-2026-11-10/payroll-api-isolated.mjs

@@ -0,0 +1,112 @@
+// Review first. Default only prepares private code; --run authorizes this fixed
+// diagnostic execution. Nothing here publishes a gateway or writes business data.
+import fs from 'node:fs';import assert from 'node:assert/strict';import {createHash,randomBytes} from 'node:crypto';
+import {patchFunctions,localCandidates,OPERATIONS_HELPERS} from '../../scripts/speed-round3-patch.mjs';
+const read=p=>fs.readFileSync(p,'utf8'),hash=s=>createHash('sha256').update(s).digest('hex');
+const privateDir='tmp/speed-round3-2026-11-10',outDir='perf/speed-round3-2026-11-10',savedDir='/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3';
+const args=process.argv.slice(2),execute=args.includes('--run'),smoke=args.includes('--smoke'),monthArg=args.find(a=>a.startsWith('--month=')),month=monthArg?.slice(8)||'2026-11';
+if(args.some(a=>!['--run','--smoke'].includes(a)&&a!==monthArg)||!/^\d{4}-(0[1-9]|1[0-2])$/.test(month))throw Error('Usage: node --import ./tmp/admin-speed-round2-2026-11-10/env.mjs perf/speed-round3-2026-11-10/payroll-api-isolated.mjs [--run] [--smoke] [--month=YYYY-MM]');
+const baseline=JSON.parse(read(savedDir+'/admin-gateway.json')),patched=patchFunctions(baseline.code,localCandidates().operations,OPERATIONS_HELPERS),definitions=JSON.parse(read(savedDir+'/original-definitions.json'));
+assert.equal(definitions.length,39,'Expected the reviewed 39 baseline PostgreSQL definitions');
+const schema='speed3_api_'+randomBytes(6).toString('hex'),company='7pIbDBJmKx';
+const rewrite=s=>s.replace(/^BEGIN;\s*$/m,'').replace(/^COMMIT;\s*$/m,'').replaceAll('public.',schema+'.').replace(/(\bnspname\s*=\s*)'public'/g,"$1'"+schema+"'").replaceAll('xs-payroll-source-cache:','isolated-'+schema+'-source:').replaceAll('xs-payroll-version-cache:','isolated-'+schema+'-version:');
+const tenantTables=['CommonModel','CourseAppointment','LessonRecord','LessonRecordFact','MemoryPracticeRecord','TeachingCompensationRule','CourseTeachingProfile','CoachPayRate','ReviewReward','ReviewRewardCorrection','TeachingCourseCategory','LegacyDirectRun','LegacyDirectEvidence','LegacySyncConflict','PayrollCutoffDecision','PayrollCutoffApproval','PayrollReadDigest','PayrollLine','PayrollBatch','_User','Node','CourseBinding'];
+const globalTables=['PayrollReadDigestMeta','PayrollReadAuthority'],emptyTables=['PayrollVerifiedReadCache','PayrollReadChange','PayrollReadChangeScope','PayrollCommand','CourseCreditReservation','UserUserPoint','PayrollReconciliation'];
+const sql={functions:definitions.map(row=>rewrite(row.definition)).join(';\n'),bulk:rewrite(read('scripts/sql/payroll-source-bulk.sql')),index:rewrite(read('scripts/sql/payroll-source-index.sql')),snapshot:rewrite(read('scripts/sql/payroll-read-snapshot.sql')),conflict:rewrite(read('scripts/sql/payroll-cutoff-bulk.sql'))};
+// No public fallback: an omitted table/function must fail, never silently read or
+// write public data. Public relations occur only in the fixed setup copy statements.
+const config={schema,company,month,tenantTables,globalTables,emptyTables,sql,operatorId:'__OPERATOR_BOUND_AT_RUN__'};
+function runtimeSupport(){
+ let activeVariant='',inFlight=new Set(),queries=[],policyRows=new Map(),unexpectedPolicies=new Set();
+ const quote=name=>'"'+name.replaceAll('"','""')+'"';
+ const settings=async tx=>{await tx.none('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');await tx.none('SET LOCAL search_path='+quote(config.schema)+',pg_catalog');await tx.none("SET LOCAL statement_timeout='120s'; SET LOCAL jit=off");};
+ const track=promise=>{inFlight.add(promise);promise.then(()=>inFlight.delete(promise),()=>inFlight.delete(promise));return promise;};
+ const dbProxy=(db,inTransaction=false)=>new Proxy({},{get(_target,name){
+  if(name==='tx')return callback=>track(db.tx(async tx=>{await settings(tx);return callback(dbProxy(tx,true));}));
+  if(!['query','one','oneOrNone','any','many','none'].includes(name))throw Error('Unexpected diagnostic DB method '+String(name));
+  return(sql,args)=>{
+   if(/\bpublic\s*\./i.test(sql)||/\bsearch_path\b/i.test(sql))throw Error('Gateway attempted to escape isolated schema');
+   if(/^SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY$/i.test(sql)){if(!inTransaction)throw Error('Missing explicit read snapshot');return db.none(sql);}
+   if(!/^\s*(SELECT|WITH)\b/i.test(sql))throw Error('Non-read gateway SQL rejected');
+   const started=Date.now();
+   const job=inTransaction?db[name](sql,args):db.tx(async tx=>{await settings(tx);return tx[name](sql,args);});
+   return track(job.then(value=>{if(sql.includes('xs_payroll_read_snapshot')&&!value?.result)throw Error('Coherent snapshot unexpectedly fell back');queries.push({method:name,ms:Date.now()-started,kind:sql.includes('xs_payroll_read_snapshot')?'coherent-snapshot':sql.includes('xs_payroll_sources_cached')?'sources-cold':sql.includes('xs_payroll_cutoff_snapshot_fast')?'cutoff-cold':sql.includes('xs_payroll_preview_cache')?'preview-cache':sql.includes('xs_payroll_read_users')?'thin-users':'other',rows:Array.isArray(value)?value.length:1});return value;}));
+  };
+ }});
+ const isolatedDb=dbProxy(Psql);
+ const sharedPolicies=async(original,className,context,input,limit=1000)=>{const key=className+':'+limit;if(!policyRows.has(key)){unexpectedPolicies.add(key);policyRows.set(key,await original(className,context,input,limit));}return policyRows.get(key);};
+ const context=()=>({roles:['ops-manager'],company:{id:config.company},current:{id:config.operatorId},isSuperAdmin:false});
+ async function setup(){return Psql.tx(async tx=>{
+  await tx.none('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');await tx.none('CREATE SCHEMA '+quote(config.schema));await tx.none('REVOKE ALL ON SCHEMA '+quote(config.schema)+' FROM PUBLIC');await tx.none('SET LOCAL search_path='+quote(config.schema)+',pg_catalog');await tx.none("SET LOCAL statement_timeout='120s'; SET LOCAL jit=off");
+  for(const table of config.tenantTables)await tx.none('CREATE TABLE '+quote(table)+' AS SELECT * FROM public.'+quote(table)+' WHERE company=$1'+(table==='CommonModel'?' AND "modelId" IN (54,59,60)':''),[config.company]);
+  for(const table of config.globalTables)await tx.none('CREATE TABLE '+quote(table)+' AS SELECT * FROM public.'+quote(table));
+  for(const table of config.emptyTables)await tx.none('CREATE TABLE '+quote(table)+' (LIKE public.'+quote(table)+' INCLUDING ALL)');
+  await tx.none('CREATE TABLE "Function" AS SELECT * FROM public."Function" WHERE path=\'xiaoshu/ops/gateway-v3\'');
+  // Keep pg_get_viewdef meaningful to baseline stamps while freezing exact rows.
+  await tx.none('CREATE TABLE "ReviewRewardIncomeSnapshot" AS SELECT * FROM public."ReviewRewardIncome" WHERE company=$1',[config.company]);await tx.none('CREATE VIEW "ReviewRewardIncome" AS SELECT * FROM "ReviewRewardIncomeSnapshot"');
+  for(const table of ['CommonModel','CourseAppointment','LessonRecord','LessonRecordFact','MemoryPracticeRecord','TeachingCompensationRule','CoachPayRate','LegacySyncConflict','_User','Node','CourseBinding','PayrollLine','PayrollBatch'])await tx.none('CREATE UNIQUE INDEX ON '+quote(table)+' ("objectId")');
+  // Teacher mappings SELECT p.* GROUP BY p.objectId: PostgreSQL recognizes the
+  // functional dependency only for a primary key, not a standalone unique index.
+  await tx.none('ALTER TABLE "CourseTeachingProfile" ADD PRIMARY KEY ("objectId")');
+  await tx.none('CREATE UNIQUE INDEX ON "PayrollReadAuthority" (name); CREATE UNIQUE INDEX ON "PayrollReadDigestMeta" (kind); CREATE UNIQUE INDEX ON "PayrollReadDigest" (kind,company,"objectId"); CREATE INDEX ON "PayrollReadDigest" (company,kind COLLATE "C",hash COLLATE "C")');
+  for(const table of ['CourseAppointment','LessonRecord','MemoryPracticeRecord'])await tx.none('CREATE INDEX ON '+quote(table)+' (company,id)');
+  await tx.none('CREATE INDEX ON "CommonModel" (company,"modelId","itemId","objectId"); CREATE INDEX ON "CommonModel" (company,"modelId",("generalId"::text)); CREATE INDEX ON "CommonModel" (company,"modelId","generalId")');
+  // Saved definitions are alphabetical, not dependency sorted. Defer body lookup
+  // until all definitions exist; copied command rowtypes are present but unused.
+  await tx.none('SET LOCAL check_function_bodies=off');await tx.none(config.sql.functions);await tx.none('SET LOCAL check_function_bodies=on');
+  // Static copied digest only. This diagnostic DOES NOT validate source writes,
+  // digest triggers, or their concurrency; no copied business source is mutated.
+  await tx.none('CREATE OR REPLACE FUNCTION xs_payroll_digest_ready() RETURNS boolean LANGUAGE sql STABLE AS $$ SELECT true $$');
+  await tx.none('UPDATE "PayrollReadAuthority" SET hash=md5(pg_get_functiondef(to_regprocedure(name)))');
+  await tx.any(config.sql.bulk);await tx.any(config.sql.index);await tx.any(config.sql.snapshot);await tx.any(config.sql.conflict);
+  for(const table of [...config.tenantTables,...config.globalTables,...config.emptyTables,'ReviewRewardIncomeSnapshot','PayrollSourceIndex','PayrollSourceIndexMeta'])await tx.none('ANALYZE '+quote(table));
+  const guards=await tx.one('SELECT xs_payroll_authority_ready() authority,xs_payroll_digest_ready() digest,xs_payroll_source_index_ready() source_index,xs_payroll_read_snapshot_authority_ready() snapshot');
+  if(Object.values(guards).some(value=>value!==true))throw Error('Clone readiness check failed');
+  return {guards,copiedTables:config.tenantTables.length+config.globalTables.length+config.emptyTables.length+2,staticDigestOnly:true};
+ });}
+ async function clearCache(){await Promise.allSettled([...inFlight]);return Psql.tx(async tx=>{await settings(tx);await tx.none('TRUNCATE "PayrollVerifiedReadCache"');const row=await tx.one('SELECT count(*)::int n FROM "PayrollVerifiedReadCache"');if(row.n!==0)throw Error('Cache not cold');return row.n;});}
+ async function run(input){
+  if(input.action==='setup')return setup();
+  if(input.action==='drop'){await Promise.allSettled([...inFlight]);await Psql.none('DROP SCHEMA IF EXISTS '+quote(config.schema)+' CASCADE');return {dropped:true};}
+  if(input.action!=='pair'||!['AB','BA'].includes(input.order))throw Error('Unknown fixed diagnostic action');
+  policyRows=new Map();unexpectedPolicies=new Set();const ctx=context(),params={month:config.month,summaryOnly:false,displayOnly:false};
+  // Use identical actual Parse objects for both variants, outside both timers.
+  for(const [name,limit] of [['TeachingCourseCategory',1000],['CourseTeachingProfile',2000]])policyRows.set(name+':'+limit,await baselineApi.originalPolicies(name,ctx,params,limit));
+  const results=[];
+  for(const variant of input.order==='AB'?['baseline','candidate']:['candidate','baseline']){
+   const coldBefore=await clearCache();activeVariant=variant;queries=[];const at=Date.now();
+   try{
+    const output=await (variant==='baseline'?baselineApi:candidateApi).overview(ctx,params),serviceMs=Date.now()-at;
+    if(output.sourceStatus?.cacheHit!==false)throw Error('Expected explicitly cold result');
+    if(output.sourceStatus?.sync?.state==='error')throw Error('Cutoff query failed; timings are invalid');
+    if(variant==='candidate'&&!queries.some(q=>q.kind==='coherent-snapshot'))throw Error('Candidate did not exercise coherent snapshot');
+    results.push({variant,serviceMs,queries:[...queries],coldBefore,output});
+   }finally{await clearCache();}
+  }
+  if(unexpectedPolicies.size)throw Error('Unprefetched Parse policy reads: '+[...unexpectedPolicies].join(','));
+  return {results,coldAfter:0};
+ }
+ return {isolatedDb,sharedPolicies,run};
+}
+function factory(code){return `(()=>{${code.replace(/\bPsql\b/g,'runtime.isolatedDb')}\nconst originalPolicies=newClassObjects;newClassObjects=(...args)=>runtime.sharedPolicies(originalPolicies,...args);return{overview:payrollOverviewData,originalPolicies};})()`;}
+function sourceFor(operatorId){return 'const config='+JSON.stringify({...config,operatorId})+';\nconst runtime=('+runtimeSupport.toString()+')();\nconst baselineApi='+factory(baseline.code)+';\nconst candidateApi='+factory(patched.code)+';\nasync function run(input){return runtime.run(input);}';}
+const normalize=value=>Array.isArray(value)?value.map(normalize):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().filter(key=>!['refreshedAt','lagSeconds','cacheHit'].includes(key)).map(key=>[key,normalize(value[key])])):value;
+const reportContext='Cold round-2 production admin gateway baseline versus selective round-3 candidate on one static real-data clone. Ten sequential pairs alternate AB/BA. Each SQL operation uses clone-only search_path and RR; explicit candidate transactions preserve their single-snapshot semantics. TeachingCourseCategory and CourseTeachingProfile Parse reads/schema lookups are prefetched and shared within each pair OUTSIDE service timers. Service time also excludes setup, cache clearing, authentication and transport; pairRequestMs includes those per-pair prefetches/clears and transport for BOTH variants. This is not full API user-experience timing. ReviewRewardIncome is frozen as an equivalent copied-row view. All clone result caches are emptied before and after every variant. Digest readiness is stubbed for an unchanged copied digest: no source-write/trigger acceptance is claimed. Operator-bound manager projection, not manager-login acceptance.';
+fs.mkdirSync(privateDir,{recursive:true,mode:0o700});new Function(sourceFor(config.operatorId));
+fs.writeFileSync(privateDir+'/payroll-api-isolated-prepared.js',sourceFor(config.operatorId),{mode:0o600});fs.writeFileSync(privateDir+'/payroll-api-isolated-patch-audit.json',JSON.stringify(patched.audit,null,2),{mode:0o600});
+if(!execute){console.log(JSON.stringify({prepared:true,willRun:false,rounds:smoke?1:10,month,script:privateDir+'/payroll-api-isolated-prepared.js',baselineHash:hash(baseline.code),candidateHash:hash(patched.code),reviewNote:'Run only after reviewing clone setup and baseline fidelity.'}));}
+else{
+ const {withGateway,operator,parse}=await import('./server-util.mjs'),me=await operator();
+ const live=await parse('/classes/Function/'+baseline.objectId);assert.equal(hash(live.code),hash(baseline.code),'Production gateway drifted; refresh reviewed baseline before running');
+ const samples=[],rounds=smoke?1:10;let setupResult,removed=false;
+ await withGateway(sourceFor(me.objectId),async call=>{try{
+  setupResult=await call({action:'setup'});
+  for(let round=0;round<rounds;round++){
+   const order=round%2?'BA':'AB',started=performance.now(),pair=await call({action:'pair',order}),requestMs=Math.round(performance.now()-started);
+   assert.equal(pair.coldAfter,0);const [a,b]=pair.results;assert.deepEqual(normalize(a.output),normalize(b.output),'Full overview output changed in round '+round);
+   for(const row of pair.results){fs.writeFileSync(privateDir+'/payroll-api-isolated-'+round+'-'+row.variant+'.json',JSON.stringify(row),{mode:0o600});const sample={round,order,variant:row.variant,serviceMs:row.serviceMs,pairRequestMs:requestMs,outputHash:hash(JSON.stringify(normalize(row.output))),coldBefore:row.coldBefore,cacheHit:row.output.sourceStatus.cacheHit,queries:row.queries};samples.push(sample);console.log(JSON.stringify({round,order,variant:sample.variant,serviceMs:sample.serviceMs,outputHash:sample.outputHash,cacheHit:sample.cacheHit}));}
+  }
+ }finally{removed=(await call({action:'drop'})).dropped===true;}});
+ const percentile=(values,p)=>values.toSorted((a,b)=>a-b)[Math.ceil(values.length*p)-1],summary=Object.fromEntries(['baseline','candidate'].map(variant=>{const rows=samples.filter(row=>row.variant===variant);return[variant,{samples:rows.length,p50Ms:percentile(rows.map(r=>r.serviceMs),.5),p75Ms:percentile(rows.map(r=>r.serviceMs),.75),p95Ms:percentile(rows.map(r=>r.serviceMs),.95)}];}));
+ fs.writeFileSync(outDir+'/payroll-api-isolated'+(smoke?'-smoke':'')+'.json',JSON.stringify({context:reportContext,month,baselineHash:hash(baseline.code),candidateHash:hash(patched.code),setup:setupResult,removed,fullOutputEquality:true,normalization:['refreshedAt','lagSeconds','cacheHit'],summary,samples},null,2));console.log(JSON.stringify({summary,removed,fullOutputEquality:true}));
+}

File diff suppressed because it is too large
+ 22 - 0
perf/speed-round3-2026-11-10/payroll-isolated-profile.mjs


+ 142 - 0
perf/speed-round3-2026-11-10/payroll-users-benchmark.json

@@ -0,0 +1,142 @@
+[
+  [
+    {
+      "candidate": false,
+      "ms": 86,
+      "bytes": 4314933,
+      "count": 1235
+    },
+    {
+      "candidate": true,
+      "ms": 97,
+      "bytes": 592386,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": true,
+      "ms": 38,
+      "bytes": 592386,
+      "count": 1235
+    },
+    {
+      "candidate": false,
+      "ms": 179,
+      "bytes": 4314933,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": false,
+      "ms": 63,
+      "bytes": 4314933,
+      "count": 1235
+    },
+    {
+      "candidate": true,
+      "ms": 30,
+      "bytes": 592386,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": true,
+      "ms": 25,
+      "bytes": 592386,
+      "count": 1235
+    },
+    {
+      "candidate": false,
+      "ms": 62,
+      "bytes": 4314933,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": false,
+      "ms": 63,
+      "bytes": 4314933,
+      "count": 1235
+    },
+    {
+      "candidate": true,
+      "ms": 22,
+      "bytes": 592386,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": true,
+      "ms": 22,
+      "bytes": 592386,
+      "count": 1235
+    },
+    {
+      "candidate": false,
+      "ms": 62,
+      "bytes": 4314933,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": false,
+      "ms": 61,
+      "bytes": 4314933,
+      "count": 1235
+    },
+    {
+      "candidate": true,
+      "ms": 38,
+      "bytes": 592386,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": true,
+      "ms": 27,
+      "bytes": 592386,
+      "count": 1235
+    },
+    {
+      "candidate": false,
+      "ms": 69,
+      "bytes": 4314933,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": false,
+      "ms": 109,
+      "bytes": 4314933,
+      "count": 1235
+    },
+    {
+      "candidate": true,
+      "ms": 42,
+      "bytes": 592386,
+      "count": 1235
+    }
+  ],
+  [
+    {
+      "candidate": true,
+      "ms": 25,
+      "bytes": 592386,
+      "count": 1235
+    },
+    {
+      "candidate": false,
+      "ms": 126,
+      "bytes": 4314933,
+      "count": 1235
+    }
+  ]
+]

+ 5 - 0
perf/speed-round3-2026-11-10/payroll-users-benchmark.mjs

@@ -0,0 +1,5 @@
+import fs from 'node:fs';import ts from 'typescript';import assert from 'node:assert/strict';import {withGateway} from './server-util.mjs';
+const source=fs.readFileSync('scripts/cloud/payroll-integrity.js','utf8');const ast=ts.createSourceFile('x.js',source,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);const helper=ast.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name.text==='payrollReadUsers').getText(ast);
+const gateway=fs.readFileSync('scripts/deploy-admin-functions.mjs','utf8').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];const support=['text','legacyOf','displayName','mobileOf'].map(name=>gateway.match(new RegExp('function '+name+'\\([^\\n]+'))?.[0]).join('\n');
+async function run(){return Psql.tx(async tx=>{await tx.none(usersSql.replace('CREATE OR REPLACE FUNCTION xs_payroll_read_users','CREATE OR REPLACE FUNCTION pg_temp.xs_payroll_read_users'));const results=[];function business(users){return users.map(u=>[u.objectId,Number(u.legacyUserId||legacyOf(u).UserID)||0,displayName(u),Number(legacyOf(u).ParentUserID)||0,mobileOf(u)]).sort((a,b)=>a[0].localeCompare(b[0]));}for(let i=0;i<10;i++){const rows=[];for(const candidate of i%2?[true,false]:[false,true]){const start=Date.now();const users=candidate?(await tx.query('SELECT pg_temp.xs_payroll_read_users($1) AS row',['7pIbDBJmKx'])).map(x=>x.row):await tx.query('SELECT * FROM "_User" WHERE "company"=$1',['7pIbDBJmKx']);rows.push({candidate,ms:Date.now()-start,bytes:JSON.stringify(users).length,business:business(users)});}if(JSON.stringify(rows[0].business)!==JSON.stringify(rows[1].business))throw Error('User hydration changed');results.push(rows.map(({business,...v})=>({...v,count:business.length})));}await tx.none('DROP FUNCTION pg_temp.xs_payroll_read_users(text)');return results;});}
+const result=await withGateway('const usersSql='+JSON.stringify(fs.readFileSync('scripts/sql/payroll-read-snapshot.sql','utf8').slice(fs.readFileSync('scripts/sql/payroll-read-snapshot.sql','utf8').indexOf('CREATE OR REPLACE FUNCTION xs_payroll_read_users')))+';'+support+'\n'+helper+'\n'+run.toString(),c=>c({}));assert.equal(result.length,10);fs.writeFileSync('perf/speed-round3-2026-11-10/payroll-users-benchmark.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result));

+ 63 - 0
perf/speed-round3-2026-11-10/release.mjs

@@ -0,0 +1,63 @@
+// Selective release. SQL adds derived structures only; authoritative definitions are pinned.
+import fs from 'node:fs';import {createHash} from 'node:crypto';import {parse,withGateway} from './server-util.mjs';import {localCandidates,patchFunctions,MOBILE_HELPERS,MONTHLY_HELPERS,OPERATIONS_HELPERS} from '../../scripts/speed-round3-patch.mjs';
+const root='/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3',action=process.argv[2];
+const files=['payroll-source-index.sql','payroll-read-snapshot.sql','payroll-cutoff-bulk.sql','teacher-earned-months.sql'];
+const sql=files.map(name=>fs.readFileSync('scripts/sql/'+name,'utf8').replace(/^BEGIN;\s*$/gm,'').replace(/^COMMIT;\s*$/gm,'')).join('\n');
+const originals=JSON.parse(fs.readFileSync(root+'/original-definitions.json')).map(({proname,definition})=>({proname,definition})).sort((a,b)=>(a.proname+a.definition).localeCompare(b.proname+b.definition));
+const names=[...new Set(originals.map(x=>x.proname))],newNames=[...new Set([...sql.matchAll(/CREATE OR REPLACE FUNCTION\s+(?:public\.)?(\w+)/g)].map(x=>x[1])), 'xs_payroll_read_snapshot_authority_ready'];
+const sha=x=>createHash('sha256').update(x).digest('hex');
+const query=`SELECT proname,pg_get_functiondef(p.oid) AS definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])`;
+const rollback=`DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "CommonModel";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "CommonModel";
+DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "CourseAppointment";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "CourseAppointment";
+DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "LessonRecord";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "LessonRecord";
+DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON "LessonRecordFact";DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON "LessonRecordFact";
+DROP TABLE IF EXISTS "PayrollSourceIndex","PayrollSourceIndexMeta","TeacherEarnedMonth";
+DROP INDEX IF EXISTS payroll_source_index_common59_digest;`;
+async function run(p){
+ const canonical=rows=>rows.sort((a,b)=>(a.proname+a.definition).localeCompare(b.proname+b.definition));
+ return Psql.tx(async tx=>{
+  await tx.none("SET LOCAL lock_timeout='3s';SET LOCAL statement_timeout='120s';");
+  if(JSON.stringify(canonical(await tx.query(config.query,[config.names])))!==JSON.stringify(config.originals))throw Error('Original SQL definitions drifted');
+  if(!(await tx.one('SELECT xs_payroll_digest_ready() AND xs_payroll_authority_ready() AS ready')).ready)throw Error('Existing digest/authority guard is not ready');
+  if(p.action==='install'){
+   if((await tx.one('SELECT EXISTS(SELECT 1 FROM "LegacyDirectRun" WHERE status=$1) active',['running'])).active)throw Error('Synchronization must be idle');
+   if((await tx.one(`SELECT to_regclass('"PayrollSourceIndex"') IS NOT NULL OR to_regclass('"PayrollSourceIndexMeta"') IS NOT NULL OR to_regclass('"TeacherEarnedMonth"') IS NOT NULL OR to_regclass('payroll_source_index_common59_digest') IS NOT NULL AS present`)).present)throw Error('Derived infrastructure already present');
+   if((await tx.one("SELECT EXISTS(SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])) AS present",[config.newNames])).present)throw Error('A proposed helper already exists');
+   if((await tx.one("SELECT EXISTS(SELECT 1 FROM pg_trigger WHERE tgrelid=ANY(ARRAY['\"CommonModel\"'::regclass,'\"CourseAppointment\"'::regclass,'\"LessonRecord\"'::regclass,'\"LessonRecordFact\"'::regclass]) AND tgname IN ('xs_payroll_source_index_row','xs_payroll_source_index_truncate')) AS present")).present)throw Error('A proposed trigger already exists');
+   await tx.any(config.sql);
+  }else if(p.action==='rehearse'||p.action==='rollback'){
+   if(p.action==='rehearse'&&!(await tx.one(`SELECT to_regclass('\"PayrollSourceIndex\"') IS NOT NULL AND to_regclass('\"PayrollSourceIndexMeta\"') IS NOT NULL AND to_regclass('\"TeacherEarnedMonth\"') IS NOT NULL AND to_regprocedure('xs_payroll_read_snapshot(text,text,boolean,boolean)') IS NOT NULL AS present`)).present)throw Error('Install first; an empty rollback is not a rehearsal');
+   await tx.none(config.rollback);
+   const functions=await tx.query("SELECT p.oid::regprocedure::text AS identity FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[])",[config.newNames]);
+   for(const fn of functions)await tx.none('DROP FUNCTION IF EXISTS '+fn.identity);
+   if(JSON.stringify(canonical(await tx.query(config.query,[config.names])))!==JSON.stringify(config.originals))throw Error('Rollback changed authoritative SQL');
+   if(p.action==='rehearse')throw Error('ROLLBACK_REHEARSAL_OK');return{rolledBack:true};
+  }
+  if(JSON.stringify(canonical(await tx.query(config.query,[config.names])))!==JSON.stringify(config.originals))throw Error('Migration changed authoritative SQL');
+  const ready=await tx.one('SELECT xs_payroll_digest_ready() digest,xs_payroll_source_index_ready() source,xs_payroll_read_snapshot_authority_ready() snapshot');
+  if(!ready.digest||!ready.source||!ready.snapshot)throw Error('Installed guard not ready');
+  const definitions=await tx.query("SELECT proname,pg_get_function_identity_arguments(p.oid) args,pg_get_functiondef(p.oid) definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname=ANY($1::text[]) ORDER BY proname,args",[config.newNames]);
+  return{ready,definitions,originalFunctionsUnchanged:true};
+ });
+}
+const source='const config='+JSON.stringify({sql,originals,names,newNames,query,rollback})+';'+run.toString();
+if(action==='prepare'){
+ const candidates=localCandidates(),manifest={sqlHash:sha(sql),rollbackHash:sha(rollback),scopes:{}};
+ for(const [scope,baseFile,helpers]of[['mobile','mobile-gateway.json',[...MOBILE_HELPERS,...MONTHLY_HELPERS]],['operations','admin-gateway.json',OPERATIONS_HELPERS]]){
+  const baseline=JSON.parse(fs.readFileSync(root+'/'+baseFile)),result=patchFunctions(baseline.code,candidates[scope],helpers);
+  fs.writeFileSync(root+'/'+scope+'-candidate.json',JSON.stringify({...baseline,code:result.code}),{mode:0o600});manifest.scopes[scope]={id:baseline.objectId,...result.audit};
+ }
+ fs.writeFileSync(root+'/infrastructure.sql',sql,{mode:0o600});fs.writeFileSync(root+'/rollback.sql',rollback,{mode:0o600});fs.writeFileSync(root+'/manifest.json',JSON.stringify(manifest,null,2),{mode:0o600});console.log(JSON.stringify({prepared:true,sqlHash:manifest.sqlHash,scopes:Object.keys(manifest.scopes)}));
+}else if(['install','check','rehearse','rollback'].includes(action)){
+ const manifest=JSON.parse(fs.readFileSync(root+'/manifest.json'));if(manifest.sqlHash!==sha(sql)||manifest.rollbackHash!==sha(rollback))throw Error('Reviewed migration changed');
+ const result=await withGateway(source,async call=>{try{return await call({action});}catch(e){if(action==='rehearse'&&e.message==='ROLLBACK_REHEARSAL_OK')return{rehearsed:true};throw e;}});
+ fs.writeFileSync(root+'/'+action+'-receipt.json',JSON.stringify(result,null,2),{mode:0o600});console.log(JSON.stringify({...result,definitions:result.definitions?.length}));
+}else if(action==='publish'){
+ const manifest=JSON.parse(fs.readFileSync(root+'/manifest.json'));
+ for(const scope of ['operations','mobile']){
+  const saved=manifest.scopes[scope],candidate=JSON.parse(fs.readFileSync(root+'/'+scope+'-candidate.json')),live=await parse('/classes/Function/'+saved.id);
+  if(sha(live.code)!==saved.baselineHash||sha(candidate.code)!==saved.candidateHash)throw Error('Gateway drift or candidate changed: '+scope);
+ }
+ const receipts=[];for(const scope of ['operations','mobile']){const saved=manifest.scopes[scope],candidate=JSON.parse(fs.readFileSync(root+'/'+scope+'-candidate.json'));await parse('/classes/Function/'+saved.id,'PUT',{code:candidate.code});const readback=await parse('/classes/Function/'+saved.id);if(sha(readback.code)!==saved.candidateHash)throw Error('Gateway readback mismatch: '+scope);receipts.push({scope,hash:saved.candidateHash});}
+ fs.writeFileSync(root+'/publish-receipt.json',JSON.stringify(receipts,null,2),{mode:0o600});console.log(JSON.stringify(receipts));
+}else throw Error('Expected prepare/install/check/rehearse/publish/rollback');

+ 44 - 0
perf/speed-round3-2026-11-10/report.md

@@ -0,0 +1,44 @@
+# 第三轮验收报告
+
+本批按用户提出的月度模型优化教师收入,同时继续降低数据变化后费用核验及学生、老师端读取成本。不能声称所有页面或完整核验都已达到 1 秒。
+
+## 可复现的配对结果
+
+| 操作与计时边界 | 前 p75 | 后 p75 | 样本 |
+|---|---:|---:|---|
+| 费用完整核验/完整汇总,清空普通结果缓存,隔离真实数据服务计时 | 6.835 s | 3.257 s | 10 对 AB/BA |
+| 教师全部月份收入,隔离真实数据服务计时 | 1.679 s | 0.677 s | 10 对 AB/BA |
+| 老师当天复习,真实记录只读投影的请求计时 | 1.819 s | 0.246 s | 10 对 AB/BA |
+| 老师陪练主页数据,同上 | 0.234 s | 0.164 s | 10 对 AB/BA |
+| 学生伴学主页数据,同上 | 0.211 s | 0.204 s | 10 对 AB/BA |
+| 学生首页所需账号资料,同上 | 0.223 s | 0.157 s | 10 对 AB/BA |
+| 老师 301 条任务全部可用,匿名夹具生产构建浏览器 | 2.295 s | 1.462 s | 10 对 AB/BA |
+
+费用完整计算下降 52.3%,20 次均未命中普通结果缓存。全部十对输出保留金额、教师、明细、异常、门店、全部历史原始数量、来源版本和截止核验版本;仅忽略刷新时间、随时间变化的延迟秒数和缓存诊断标志,逐字段比较完全相等。该服务对照使用同一份隔离数据库复制数据,排除了身份验证、网络、准备/清理和两类预取 Parse 元数据时间,不能当作浏览器完整打开耗时。生产从真实用户点击到可用还受网络、设备等影响。
+
+移动只读投影比较实际已有教师/学生记录,鉴权投影绑定当前授权操作员,没有伪造教师/学生会话;学生资料对照双方都关闭旧系统同步写入,相关同步成本未计入。浏览器分页数据为匿名固定夹具,两角色五条页面旅程的内容与控件一致;390/1024 两种宽度共20组截图,18组完全相同,2组仅卡片边界一行像素差。
+
+组合方案在包含1083条课次、28个月的实际教师数据上,收入服务 p75 为1.679→0.677秒,下降59.7%;首次冷月度汇总742ms,28个月全部计算,后9次28个月均匹配且金额计算次数为0。每次仍读取并验证当前来源,全部20个响应的收入字段一致。该隔离服务计时包含来源、规则、映射、复习和月度SQL,排除鉴权与网络,非页面打开时间。
+
+## 按月计算怎样保持准确
+
+- 保留原收入定义:应得课时收入 + 当前有效复习收益。不是用审核/发放账单替代应得收入,也不把历史草稿自动变成已结算。
+- 未变化历史月份保存已核验月度金额和待核对数量,当前月份持续更新。历史补录、金额更正、删除、跨月移动、规则/算法变化会使受影响月份重新计算。
+- 全局重复课次判断仍先于月度拆分;整数分累加,未知日期保守保留;来源与摘要代次同一事务快照,避免旧计算覆盖新摘要。
+- 真实运行时验证过24个月:全部不变时0次金额重算;单月更正及撤销只重算1个月。四个收入字段与原算法逐值相同。该模拟更正只发生在隔离响应内存,没有改生产工资数据。
+- 每次仍验证当前来源依赖,绝非仅靠缓存有效期。索引落后或不存在时用当前原始记录补算。原始工资生成、审核、付款路径及核验规则保持不变。
+
+## 并发与保存速度
+
+真实 PostgreSQL 隔离 schema 安装了真实摘要触发器;两事务分别以 READ COMMITTED / REPEATABLE READ 交错提交,故意让旧投影后提交。提交前后索引都保持可用,完整有序来源及版本与原 SQL 一致。另验证 CommonModel+历史补充的并发插入、删除主记录后补充重新出现、跨公司移动,未发生隐藏记录被计薪。
+
+发现并移除了“课次变化时维护隐藏历史补充”的重复工作。有限样本中,单课次维护耗时为3~13ms;关联4~5课次的预约更新33~39ms(早期方案585~710ms,已弃用);这些为写入成本抽样,不是p75。无公司级业务写锁,派生维护争用时不阻塞另一源写入,读取通过当前依赖验证保证准确。
+
+## 检查与上线边界
+
+- 移动端全量350项通过,生产构建成功,11个云函数定义验证通过;构建只有既有样式预算与html2pdf提示。
+- 后端财务/月度/移动读取/补丁测试102项通过;来源写端简化后索引与补丁复验另行通过,结果见发布记录。
+- 选择性 AST 补丁仅包含本批移动端14个、管理端6个函数;其余线上函数声明和执行语句保持一致。数据库39个原有函数定义在安装/回滚演练前后比对。
+- 静态发布为学生/老师共用的移动H5,管理端本批无静态变更;没有重新打原生安装包,没有真实手机设备或真实教师/学生密码登录验收。
+
+发布记录将补充公网哈希、真实管理端十一模块、费用明细/导出、移动端入口与回滚演练结果。详细脚本与计时排除项见同目录JSON和README。

+ 6 - 0
perf/speed-round3-2026-11-10/server-util.mjs

@@ -0,0 +1,6 @@
+import fs from 'node:fs';import {randomBytes} from 'node:crypto';
+export const env={...process.env};for(const line of fs.readFileSync('/Users/mac/.config/xiaoshu/legacy-sync-nightly.env','utf8').split('\n')){const m=line.match(/^(?:export\s+)?(XIAOSHU_[A-Z0-9_]+)=(.*)$/);if(m&&!env[m[1]])env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');}
+export const base='https://server.xiaoshu.pro',token=env.XIAOSHU_SYNC_OPERATOR_TOKEN;
+export async function parse(path,method='GET',body){if(!env.XIAOSHU_MASTER_KEY)throw Error('Authorized configuration missing');const r=await fetch(base+'/parse'+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':env.XIAOSHU_PARSE_APP_ID,'X-Parse-Master-Key':env.XIAOSHU_MASTER_KEY},body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)});const data=await r.json();if(!r.ok||data.error)throw Error('Parse request failed '+r.status);return data;}
+export async function operator(){const r=await fetch(base+'/parse/users/me',{headers:{'X-Parse-Application-Id':env.XIAOSHU_PARSE_APP_ID,'X-Parse-Session-Token':token},signal:AbortSignal.timeout(15000)});const user=await r.json();if(!r.ok||!user.objectId)throw Error('Operator session invalid');return user;}
+export async function withGateway(source,task){new Function(source);const me=await operator(),path='xiaoshu/system/speed3-'+randomBytes(10).toString('hex');const code=source+`\nasync function handler(request,response){if(request.user?.id!==${JSON.stringify(me.objectId)}||Date.now()>${Date.now()+3600000})return response.status(403).json({success:false});try{return response.json({success:true,data:await run(request.body?.params||{})});}catch(e){return response.status(500).json({success:false,message:e.message});}}`;const row=await parse('/classes/Function','POST',{name:path,path,code,type:'standalone',enabled:true,params:[],paramList:[],respType:'json'});try{return await task(async params=>{const r=await fetch(base+'/api/functions/'+path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params}),signal:AbortSignal.timeout(180000)});const data=await r.json();if(!r.ok||!data.success)throw Error(String(data.message||'Diagnostic operation failed').slice(0,400));return data.data;},{path,me});}finally{await parse('/classes/Function/'+row.objectId,'DELETE');}}

+ 210 - 0
perf/speed-round3-2026-11-10/source-index-concurrency.json

@@ -0,0 +1,210 @@
+{
+  "date": "2026-11-10",
+  "environment": "Private PostgreSQL clone of current production data; real digest and projection triggers; original sources unchanged",
+  "anonymized": true,
+  "concurrency": [
+    {
+      "isolation": "READ COMMITTED",
+      "targetLockSkipped": true,
+      "sourceWriteMs": 29,
+      "staleProjectionConfirmed": true,
+      "whileFirstWriterUncommitted": {
+        "guardsReady": true,
+        "orderedRowsIdentical": true,
+        "fullSourceVersionIdentical": true
+      },
+      "afterBothCommits": {
+        "guardsReady": true,
+        "orderedRowsIdentical": true,
+        "fullSourceVersionIdentical": true
+      }
+    },
+    {
+      "isolation": "REPEATABLE READ",
+      "targetLockSkipped": true,
+      "sourceWriteMs": 106,
+      "staleProjectionConfirmed": true,
+      "whileFirstWriterUncommitted": {
+        "guardsReady": true,
+        "orderedRowsIdentical": true,
+        "fullSourceVersionIdentical": true
+      },
+      "afterBothCommits": {
+        "guardsReady": true,
+        "orderedRowsIdentical": true,
+        "fullSourceVersionIdentical": true
+      }
+    }
+  ],
+  "membership": {
+    "deleted": {
+      "guardsReady": true,
+      "orderedRowsIdentical": true,
+      "fullSourceVersionIdentical": true
+    },
+    "movedFrom": {
+      "guardsReady": true,
+      "orderedRowsIdentical": true,
+      "fullSourceVersionIdentical": true
+    },
+    "movedTo": {
+      "guardsReady": true,
+      "orderedRowsIdentical": true,
+      "fullSourceVersionIdentical": true
+    }
+  },
+  "phantom": {
+    "beforeFirstCommit": {
+      "guardsReady": true,
+      "orderedRowsIdentical": true,
+      "fullSourceVersionIdentical": true
+    },
+    "afterBothCommits": {
+      "guardsReady": true,
+      "orderedRowsIdentical": true,
+      "fullSourceVersionIdentical": true
+    },
+    "derivedFactStillExists": true,
+    "authoritativeFactVisible": false
+  },
+  "writerSamplesPerRound": 18,
+  "writerPairs": [
+    {
+      "case": "lesson_1",
+      "table": "LessonRecord",
+      "model": null,
+      "linkedLessons": null,
+      "beforeSimplification": {
+        "withoutIndexMs": 2,
+        "withIndexMs": 155
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 3,
+        "withIndexMs": 5
+      }
+    },
+    {
+      "case": "lesson_2",
+      "table": "LessonRecord",
+      "model": null,
+      "linkedLessons": null,
+      "beforeSimplification": {
+        "withoutIndexMs": 1,
+        "withIndexMs": 18
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 2,
+        "withIndexMs": 13
+      }
+    },
+    {
+      "case": "lesson_3",
+      "table": "LessonRecord",
+      "model": null,
+      "linkedLessons": null,
+      "beforeSimplification": {
+        "withoutIndexMs": 2,
+        "withIndexMs": 172
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 3,
+        "withIndexMs": 3
+      }
+    },
+    {
+      "case": "appointment_1",
+      "table": "CourseAppointment",
+      "model": null,
+      "linkedLessons": 5,
+      "beforeSimplification": {
+        "withoutIndexMs": 1,
+        "withIndexMs": 701
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 2,
+        "withIndexMs": 39
+      }
+    },
+    {
+      "case": "appointment_2",
+      "table": "CourseAppointment",
+      "model": null,
+      "linkedLessons": 5,
+      "beforeSimplification": {
+        "withoutIndexMs": 3,
+        "withIndexMs": 710
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 4,
+        "withIndexMs": 33
+      }
+    },
+    {
+      "case": "appointment_3",
+      "table": "CourseAppointment",
+      "model": null,
+      "linkedLessons": 4,
+      "beforeSimplification": {
+        "withoutIndexMs": 1,
+        "withIndexMs": 585
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 2,
+        "withIndexMs": 34
+      }
+    },
+    {
+      "case": "common_1",
+      "table": "CommonModel",
+      "model": 54,
+      "linkedLessons": null,
+      "beforeSimplification": {
+        "withoutIndexMs": 7,
+        "withIndexMs": 181
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 10,
+        "withIndexMs": 20
+      }
+    },
+    {
+      "case": "common_2",
+      "table": "CommonModel",
+      "model": 59,
+      "linkedLessons": null,
+      "beforeSimplification": {
+        "withoutIndexMs": 2,
+        "withIndexMs": 17
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 3,
+        "withIndexMs": 8
+      }
+    },
+    {
+      "case": "common_3",
+      "table": "CommonModel",
+      "model": 60,
+      "linkedLessons": null,
+      "beforeSimplification": {
+        "withoutIndexMs": 4,
+        "withIndexMs": 2
+      },
+      "afterSimplification": {
+        "withoutIndexMs": 7,
+        "withIndexMs": 3
+      }
+    }
+  ],
+  "cleanup": {
+    "beforeCloneDropped": true,
+    "afterCloneDropped": true,
+    "temporaryGatewaysRemoved": true
+  },
+  "limits": [
+    "Nine paired writer examples per round, not a latency percentile or capacity benchmark.",
+    "Highest sampled appointment fanout was five canonical lessons.",
+    "Writer comparisons alternate enabled/disabled order; database and runtime scheduling can still affect individual samples.",
+    "These are private-clone database measurements, not production UI or device acceptance."
+  ]
+}

+ 49 - 0
perf/speed-round3-2026-11-10/source-index-report.md

@@ -0,0 +1,49 @@
+# Source projection index: correctness and write-cost verification
+
+Verified on 2026-11-10 using a private PostgreSQL copy of current production source data. The copy used the real existing row-digest triggers and the new projection triggers; readiness functions were not replaced with unconditional success. Production source tables were only read to create the copy. Both temporary schemas and their restricted diagnostic gateways were removed after verification.
+
+The public receipt is [source-index-concurrency.json](source-index-concurrency.json). It omits company identifiers, record identifiers, temporary schema names, and data fingerprints.
+
+## Correctness experiments
+
+- **READ COMMITTED and REPEATABLE READ:** Writer A changed a lesson and held its projection target lock. Writer B changed the related appointment, skipped the unavailable projection lock, and committed its real source change. Writer A committed last, leaving a confirmed stale projection. Before A committed and after both commits, current ordered source JSON and the complete source version matched the original SQL exactly; all guards remained ready.
+- **Concurrent fact insertion:** Writer A changed a canonical CommonModel mapping while writer B inserted a fact for the new effective legacy identity. After both committed, the fact remained present in the derived index but was absent from authoritative source output. Original and indexed source JSON/version matched before and after the first commit.
+- **Canonical deletion:** Removing the canonical lesson made the historical fact visible again. Original and indexed results remained identical.
+- **Tenant move:** Moving a lesson between two test companies preserved exact source output and versions for both the departure and destination companies. The changes were rolled back.
+- **Local regressions:** 41 targeted payroll, bulk-source, and projection-index tests passed, including missing/stale/orphaned projections, zero pay snapshots, schema and trigger drift, full reseeding, unknown dates, filtered metadata, and hidden fact rows.
+
+## Write-cost correction
+
+The first implementation unnecessarily refreshed fact visibility from lesson writes and searched canonical lessons while computing each fact projection. This added up to 710 ms for an appointment touching five lessons. Fact projection content depends only on the fact and selected appointment; canonical existence is already established independently at every read. Removing that redundant propagation and lookup preserved all exact-read checks while reducing the sampled write overhead. CommonModel changes outside models 54 and 59 now return before JSON serialization or metadata reads.
+
+Each round contains **18 UPDATE observations: nine paired examples with the new trigger enabled and disabled**. Original digest triggers remained enabled. Samples used alternating pair order and rolled back their changes. Times are database-call wall-clock milliseconds.
+
+| Anonymous example | Canonical lesson fanout | Before: off / on | After: off / on |
+|---|---:|---:|---:|
+| lesson_1 | — | 2 / 155 | 3 / 5 |
+| lesson_2 | — | 1 / 18 | 2 / 13 |
+| lesson_3 | — | 2 / 172 | 3 / 3 |
+| appointment_1 | 5 | 1 / 701 | 2 / 39 |
+| appointment_2 | 5 | 3 / 710 | 4 / 33 |
+| appointment_3 | 4 | 1 / 585 | 2 / 34 |
+| common_1 (model 54) | — | 7 / 181 | 10 / 20 |
+| common_2 (model 59) | — | 2 / 17 | 3 / 8 |
+| common_3 (model 60) | — | 4 / 2 | 7 / 3 |
+
+With the final implementation, lesson updates measured **3–13 ms**, appointments touching four or five canonical lessons measured **33–39 ms**, and sampled CommonModel updates measured **3–20 ms**. These are individual paired samples, not p75/p95 estimates. The results do not establish sustained synchronization throughput, production response latency, or browser/device acceptance.
+
+## Safety boundary
+
+The derived index never determines which source rows exist or which facts are suppressed. Every read rebuilds those relationships from current source rows, checks the current dependency signature, and only then reuses a stored projection. Missing or stale entries are recalculated in the reader’s snapshot; obsolete extra entries cannot create payable source rows. Writers use nonblocking per-target advisory locks and may skip projection upkeep.
+
+TRUNCATE and metadata/authority drift cause authoritative fallback. Re-enabling acceleration requires a new generation and a complete reseed, rather than changing a readiness flag. The experiments changed no settlement, payment, or original financial-function definitions.
+
+## Reproduction
+
+Run the explicit manual runner only in the authorized environment:
+
+```sh
+node --import ./tmp/admin-speed-round2-2026-11-10/env.mjs scripts/tests/run-payroll-source-index-concurrency.mjs
+```
+
+The runner accepts no arbitrary SQL from its remote caller, restricts mutation to a generated private `speed3_` schema, and removes the schema and temporary gateway in `finally` blocks.

+ 282 - 0
perf/speed-round3-2026-11-10/teacher-api-isolated.json

@@ -0,0 +1,282 @@
+{
+  "context": "Existing teacher with most LessonRecord rows in one fixed real-data clone. Original production teacherIncomeSummary versus source-index plus monthly-summary candidate. Ten AB/BA pairs; first candidate sample is cold derived months, subsequent unchanged samples intentionally reuse historical month calculations after fresh source validation. PayrollVerifiedReadCache is empty before/after each sample. Clone static digest readiness is stubbed; no source write/trigger acceptance. Service timing includes teacher source/rules/mapping/review/month SQL and calculation; excludes subject selection, setup, cache clearing, authentication and transport. Operator read projection, not actual teacher login or full UI experience.",
+  "baselineHash": "40e2007aa417b59793dde11e57b994e3fd266957d2e21dca606647c33702c194",
+  "candidateHash": "c9c315815911376c43fa2e480e90adb7a9e41712ecd45bbfdc610fc8e2f019a2",
+  "migrationHashes": {
+    "payroll-source-bulk.sql": "190bbe1557baa339359cab9eb21b9378ccbea98bf26d755cb2cf273bd33a511e",
+    "payroll-source-index.sql": "2fedfa70c2686f198def80327482236ecf33b5534716543874bfbfba8aed2277",
+    "payroll-read-snapshot.sql": "d680f090edd65796277c11f46f13e4ebca4b37a57e46d43938fcd8ce3f8653fd",
+    "payroll-cutoff-bulk.sql": "dc1d25df1d906fe52c0c2fd04b96ea183415e276e2fd77a461f725d0463682c5"
+  },
+  "setup": {
+    "guards": {
+      "authority": true,
+      "digest": true,
+      "source_index": true,
+      "snapshot": true
+    },
+    "copiedTables": 33,
+    "staticDigestOnly": true,
+    "crypto": "object",
+    "subtle": true,
+    "TextEncoder": "function"
+  },
+  "removed": true,
+  "exactGross": true,
+  "summary": {
+    "baseline": {
+      "samples": 10,
+      "p50Ms": 1619,
+      "p75Ms": 1679,
+      "p95Ms": 1817
+    },
+    "candidate": {
+      "samples": 10,
+      "p50Ms": 646,
+      "p75Ms": 677,
+      "p95Ms": 742
+    }
+  },
+  "samples": [
+    {
+      "round": 0,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 1817,
+      "pairRequestMs": 3006,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 0,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 742,
+      "pairRequestMs": 3006,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 0,
+        "computed": 28
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 1,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 677,
+      "pairRequestMs": 2627,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 1,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 1596,
+      "pairRequestMs": 2627,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 2,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 1626,
+      "pairRequestMs": 2573,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 2,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 624,
+      "pairRequestMs": 2573,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 3,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 642,
+      "pairRequestMs": 2708,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 3,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 1769,
+      "pairRequestMs": 2708,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 4,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 1619,
+      "pairRequestMs": 2732,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 4,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 731,
+      "pairRequestMs": 2732,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 5,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 675,
+      "pairRequestMs": 2603,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 5,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 1618,
+      "pairRequestMs": 2603,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 6,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 1679,
+      "pairRequestMs": 2618,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 6,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 621,
+      "pairRequestMs": 2618,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 7,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 646,
+      "pairRequestMs": 2620,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 7,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 1636,
+      "pairRequestMs": 2620,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 8,
+      "order": "AB",
+      "variant": "baseline",
+      "serviceMs": 1549,
+      "pairRequestMs": 2495,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 8,
+      "order": "AB",
+      "variant": "candidate",
+      "serviceMs": 662,
+      "pairRequestMs": 2495,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 9,
+      "order": "BA",
+      "variant": "candidate",
+      "serviceMs": 606,
+      "pairRequestMs": 2471,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": {
+        "available": true,
+        "matched": 28,
+        "computed": 0
+      },
+      "sourceLessonCount": 1083
+    },
+    {
+      "round": 9,
+      "order": "BA",
+      "variant": "baseline",
+      "serviceMs": 1596,
+      "pairRequestMs": 2471,
+      "outputHash": "68375fa3fa4f8d4ee05d3f1212214f3ab42ac2ebcf66c17780799474769d8072",
+      "monthlyReuse": null,
+      "sourceLessonCount": 1083
+    }
+  ]
+}

+ 49 - 0
perf/speed-round3-2026-11-10/teacher-api-isolated.mjs

@@ -0,0 +1,49 @@
+// Companion to payroll-api-isolated.mjs. Prepare that script first to supply the
+// reviewed clone setup. Default prepares only; --run performs the fixed diagnostic.
+import fs from 'node:fs';import assert from 'node:assert/strict';import {createHash,randomBytes} from 'node:crypto';import ts from 'typescript';
+import {patchFunctions,localCandidates,MONTHLY_HELPERS} from '../../scripts/speed-round3-patch.mjs';
+const read=p=>fs.readFileSync(p,'utf8'),hash=s=>createHash('sha256').update(s).digest('hex'),dir='tmp/speed-round3-2026-11-10',outDir='perf/speed-round3-2026-11-10';
+const args=process.argv.slice(2);if(args.some(arg=>!['--run','--smoke'].includes(arg)))throw Error('Usage: teacher-api-isolated.mjs [--run] [--smoke]');
+const baseline=JSON.parse(read('/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3/mobile-gateway.json')),candidate=patchFunctions(baseline.code,localCandidates().mobile,MONTHLY_HELPERS);
+let prepared=read(dir+'/payroll-api-isolated-prepared.js');const tree=ts.createSourceFile('prepared.js',prepared,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS),statement=tree.statements[0];assert.ok(ts.isVariableStatement(statement));const declaration=statement.declarationList.declarations[0];assert.equal(declaration.name.getText(tree),'config');let config=JSON.parse(declaration.initializer.getText(tree));
+const schema='speed3_teacher_api_'+randomBytes(6).toString('hex'),oldSchema=config.schema;config=JSON.parse(JSON.stringify(config).replaceAll(oldSchema,schema));
+// Keep the reviewed setup code, but always install CURRENT candidate migrations.
+const rewrite=s=>s.replace(/^BEGIN;\s*$/m,'').replace(/^COMMIT;\s*$/m,'').replaceAll('public.',schema+'.').replace(/(\bnspname\s*=\s*)'public'/g,"$1'"+schema+"'");
+const migrationHashes={};for(const [key,file] of [['bulk','payroll-source-bulk.sql'],['index','payroll-source-index.sql'],['snapshot','payroll-read-snapshot.sql'],['conflict','payroll-cutoff-bulk.sql']]){const raw=read('scripts/sql/'+file);config.sql[key]=rewrite(raw);migrationHashes[file]=hash(raw);}
+prepared='const config='+JSON.stringify(config)+';'+prepared.slice(statement.end).replaceAll(oldSchema,schema);
+const monthlySql=read('scripts/sql/teacher-earned-months.sql');
+function teacherFactory(code,record){return `(()=>{${code.replace(/\bPsql\b/g,'runtime.isolatedDb')}\nlet observed=null;${record?'const actualMonthly=teacherMonthlyLessonEarnings;teacherMonthlyLessonEarnings=async(...args)=>{const result=await actualMonthly(...args);observed=result.monthlyReuse;return result;};teacherMonthlyLessonEarnings.toString=()=>actualMonthly.toString();':''}return{summary:teacherIncomeSummary,getMonthly:()=>observed,reset:()=>{observed=null;}};})()`;}
+function extension(){
+ const quote=name=>'"'+name.replaceAll('"','""')+'"';
+ const scoped=callback=>Psql.tx(async db=>{await db.none('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');await db.none('SET LOCAL search_path='+quote(config.schema)+',pg_catalog');return callback(db);});
+ const clear=()=>scoped(async db=>{await db.none('TRUNCATE "PayrollVerifiedReadCache"');const value=await db.one('SELECT count(*)::int n FROM "PayrollVerifiedReadCache"');if(value.n!==0)throw Error('Result cache not cold');});
+ async function run(input){
+  if(input.action==='setup'){const result=await runtime.run(input);await scoped(db=>db.none(teacherMonthlySql));return{...result,crypto:typeof crypto,subtle:typeof crypto!=='undefined'&&!!crypto.subtle,TextEncoder:typeof TextEncoder};}
+  if(input.action==='drop')return runtime.run(input);
+  if(input.action!=='teacher-pair'||!['AB','BA'].includes(input.order))throw Error('Unknown fixed diagnostic');
+  const selected=await scoped(db=>db.one('SELECT u."legacyUserId" AS coach,c.count FROM "_User" u JOIN (SELECT jsmz::text coach,count(*)::int count FROM "LessonRecord" WHERE company=$1 GROUP BY jsmz::text) c ON c.coach=u."legacyUserId"::text WHERE u.company=$1 AND u."legacyGroupId"=3 AND NOT COALESCE(u."isDeleted",false) ORDER BY c.count DESC,u."objectId" LIMIT 1',[config.company]));
+  const results=[];for(const variant of input.order==='AB'?['baseline','candidate']:['candidate','baseline']){
+   await clear();const api=variant==='baseline'?teacherBaselineApi:teacherCandidateApi;api.reset();const at=Date.now();
+   try{const output=await api.summary(config.company,Number(selected.coach));results.push({variant,serviceMs:Date.now()-at,output,monthlyReuse:api.getMonthly(),sourceLessonCount:selected.count});}finally{await clear();}
+  }
+  return {results};
+ }
+ return {run};
+}
+const source=prepared+'\nconst teacherMonthlySql='+JSON.stringify(monthlySql)+';\nconst teacherBaselineApi='+teacherFactory(baseline.code,false)+';\nconst teacherCandidateApi='+teacherFactory(candidate.code,true)+';\nconst teacherRuntime=('+extension.toString()+')();\nasync function run(input){return teacherRuntime.run(input);}';new Function(source);
+fs.writeFileSync(dir+'/teacher-api-isolated-prepared.js',source,{mode:0o600});
+if(!args.includes('--run'))console.log(JSON.stringify({prepared:true,willRun:false,rounds:args.includes('--smoke')?1:10,baselineHash:hash(baseline.code),candidateHash:hash(candidate.code),script:dir+'/teacher-api-isolated-prepared.js'}));
+else{
+ const {withGateway,parse}=await import('./server-util.mjs'),live=await parse('/classes/Function/'+baseline.objectId);assert.equal(hash(live.code),hash(baseline.code),'Production mobile gateway drifted');
+ const samples=[],rounds=args.includes('--smoke')?1:10;let setup,removed=false;
+ await withGateway(source,async call=>{try{
+  setup=await call({action:'setup'});assert.equal(setup.subtle,true);assert.equal(setup.TextEncoder,'function');
+  for(let round=0;round<rounds;round++){
+   const order=round%2?'BA':'AB',at=performance.now(),pair=await call({action:'teacher-pair',order}),pairRequestMs=Math.round(performance.now()-at),[a,b]=pair.results;assert.deepEqual(a.output,b.output,'Teacher gross differs in round '+round);
+   const monthly=pair.results.find(row=>row.variant==='candidate').monthlyReuse;assert.equal(monthly?.available,true);if(round===0)assert.ok(monthly.computed>0,'First monthly sample was not cold');else assert.equal(monthly.computed,0,'Unchanged historical months recomputed');
+   for(const row of pair.results){fs.writeFileSync(dir+'/teacher-api-isolated-'+round+'-'+row.variant+'.json',JSON.stringify(row),{mode:0o600});const sample={round,order,variant:row.variant,serviceMs:row.serviceMs,pairRequestMs,outputHash:hash(JSON.stringify(row.output)),monthlyReuse:row.monthlyReuse,sourceLessonCount:row.sourceLessonCount};samples.push(sample);console.log(JSON.stringify(sample));}
+  }
+ }finally{removed=(await call({action:'drop'})).dropped===true;}});
+ const percentile=(values,p)=>values.toSorted((a,b)=>a-b)[Math.ceil(values.length*p)-1],summary=Object.fromEntries(['baseline','candidate'].map(variant=>{const values=samples.filter(row=>row.variant===variant).map(row=>row.serviceMs);return[variant,{samples:values.length,p50Ms:percentile(values,.5),p75Ms:percentile(values,.75),p95Ms:percentile(values,.95)}];}));
+ fs.writeFileSync(outDir+'/teacher-api-isolated'+(rounds===1?'-smoke':'')+'.json',JSON.stringify({context:'Existing teacher with most LessonRecord rows in one fixed real-data clone. Original production teacherIncomeSummary versus source-index plus monthly-summary candidate. Ten AB/BA pairs; first candidate sample is cold derived months, subsequent unchanged samples intentionally reuse historical month calculations after fresh source validation. PayrollVerifiedReadCache is empty before/after each sample. Clone static digest readiness is stubbed; no source write/trigger acceptance. Service timing includes teacher source/rules/mapping/review/month SQL and calculation; excludes subject selection, setup, cache clearing, authentication and transport. Operator read projection, not actual teacher login or full UI experience.',baselineHash:hash(baseline.code),candidateHash:hash(candidate.code),migrationHashes,setup,removed,exactGross:true,summary,samples},null,2));console.log(JSON.stringify({summary,removed,exactGross:true}));
+}

+ 66 - 0
perf/speed-round3-2026-11-10/teacher-monthly-helper-metadata.json

@@ -0,0 +1,66 @@
+{
+  "generator": "scripts/teacher-earnings-transform.mjs:teacherEarningsSupport(operationsGatewayCode,payrollIntegrityCode)",
+  "migration": "scripts/sql/teacher-earned-months.sql",
+  "helpers": [
+    {
+      "name": "teacherEarningsCalculator",
+      "operation": "replace",
+      "baselineSha256": "adf3a7bbd74465a93de42b90b1ec304b3d370e0e90c14fc39fd362fd364c7ce5",
+      "candidateSha256": "96c6ae615d3f47100d41f92afd654e69b90b3ae1ee9a37a3795beea2cb2b4d09"
+    },
+    {
+      "name": "teacherEarningsSourceRows",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "6226a875a4873e33ca96f0c66e4bf895f6eb9541986acd1298f51e1f3b0e5321"
+    },
+    {
+      "name": "teacherEarningsInputs",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "4f909eb51c872cc2450f55ab041f3191f9be5c3e47817bfcf870a5948f372984"
+    },
+    {
+      "name": "teacherPrepareEarnings",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "c6384d69017593d8a40e0f35dfe091a23550c03009468c146841377aa899f50b"
+    },
+    {
+      "name": "teacherCalculatePreparedEarnings",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "1d4c7d0fc4d2395c6f8f142a06ef92839ff31b4bd2674095c17f4a0cf5626f70"
+    },
+    {
+      "name": "teacherEarningsFingerprint",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "906f0baa38a5486e15627eaa40af5ccad5a7937457ba1aa3cc676cd085fc86ca"
+    },
+    {
+      "name": "teacherMonthlyEarningsInputs",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "88173f2d530335752d22daa4c61ba2e2ecce860927f856dbf45722ae76536bbb"
+    },
+    {
+      "name": "teacherMonthlyLessonEarnings",
+      "operation": "add",
+      "baselineSha256": null,
+      "candidateSha256": "b9e4b9631b9269c20fd84756180f09e9690f2e0263d26b07ed6b8e4b1d2e40bb"
+    },
+    {
+      "name": "teacherLessonEarnings",
+      "operation": "replace",
+      "baselineSha256": "ba09758e5b96c81153e92e34ad45059075a7f454d412c03f4b3950fcea75a27a",
+      "candidateSha256": "ebfda4ee8241f4fa2bae2abd2f38bddc4d2ad0a4c3e3d2f5057feb2771d1b554"
+    },
+    {
+      "name": "teacherIncomeSummary",
+      "operation": "replace",
+      "baselineSha256": "581ab32bb133315739e32b93da2a3b2f2ed94753e5ffcca2e0de5183d4e17535",
+      "candidateSha256": "aba9eb0242d5bf0a810d0aaa34149f38f891e3e890e440dfa019dd8c18ea631e"
+    }
+  ]
+}

+ 10 - 0
perf/speed-round3-2026-11-10/teacher-monthly-release.mjs

@@ -0,0 +1,10 @@
+// Generates reviewable local patch material only; no network or deployment.
+import fs from 'node:fs';import ts from 'typescript';import {createHash} from 'node:crypto';import {teacherEarningsSupport} from '../../scripts/teacher-earnings-transform.mjs';
+const read=p=>fs.readFileSync(p,'utf8'),hash=s=>createHash('sha256').update(s).digest('hex');
+const operations=read('scripts/deploy-admin-functions.mjs').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1],calculator=teacherEarningsSupport(operations,read('scripts/cloud/payroll-integrity.js'));
+const helperNames=['teacherEarningsCalculator','teacherEarningsSourceRows','teacherEarningsInputs','teacherPrepareEarnings','teacherCalculatePreparedEarnings','teacherEarningsFingerprint','teacherMonthlyEarningsInputs','teacherMonthlyLessonEarnings','teacherLessonEarnings','teacherIncomeSummary'];
+const select=code=>{const ast=ts.createSourceFile('helpers.js',code,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);return new Map(ast.statements.filter(ts.isFunctionDeclaration).map(node=>[node.name?.text,node.getText(ast)]));};
+const before=select(JSON.parse(read('/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3/mobile-gateway.json')).code),after=select(calculator+'\n'+read('scripts/cloud/teacher-earnings.js'));
+const patch=helperNames.map(name=>{if(!after.has(name))throw Error('Missing helper '+name);return{name,operation:before.has(name)?'replace':'add',baselineSha256:before.has(name)?hash(before.get(name)):null,candidateSha256:hash(after.get(name)),code:after.get(name)};});
+const dir='tmp/speed-round3-2026-11-10';fs.mkdirSync(dir,{recursive:true});fs.writeFileSync(dir+'/teacher-monthly-helper-patch.json',JSON.stringify(patch,null,2),{mode:0o600});
+fs.writeFileSync('perf/speed-round3-2026-11-10/teacher-monthly-helper-metadata.json',JSON.stringify({generator:'scripts/teacher-earnings-transform.mjs:teacherEarningsSupport(operationsGatewayCode,payrollIntegrityCode)',migration:'scripts/sql/teacher-earned-months.sql',helpers:patch.map(({code,...metadata})=>metadata)},null,2));console.log(JSON.stringify({helpers:patch.length,output:dir+'/teacher-monthly-helper-patch.json'}));

+ 17 - 0
perf/speed-round3-2026-11-10/teacher-monthly-report.md

@@ -0,0 +1,17 @@
+# 教师收入月度复用验证
+
+保留 `lessonIncome/reviewIncome/totalIncome/pendingPayCount` 的原始 earned 口径。没有将草稿工资单当已结算账本,没有冻结历史月份,也没有读取或改写 PayrollBatch/PayrollLine。复习收入仍每次读取 earned 账本合计,调整项保持原先含义。
+
+每次在同一 REPEATABLE READ READ ONLY 快照中读取新鲜来源、工资规则、课程映射和已存月份摘要。先按原先全局顺序选重复课次代表,再按原计算器的上海业务日期分月。月签名覆盖完整原始重复组、规则、映射、完整计算器闭包和月度实现。没有 TTL;旧月更正、记录移月、删除、未知日期和算法变动均会安全失效。写入仅是事务外派生摘要 CAS;摘要世代与来源同快照读取,防止旧读者看到较新摘要后将其覆盖回旧世代。
+
+`teacher-monthly-runtime.mjs` 使用固定 operator-bound 临时 Function 和现有教师。它不是教师登录验收。派生表位于随机隔离 schema,finally 删除 schema 和 Function;没有业务数据写入。纠正阶段只在查询返回内存中修改一条有效课次。真实运行时支持 crypto.subtle 和 TextEncoder。
+
+真实教师包含 24 个月、222 条代表课次。四阶段原版和候选版全部收入字段一致:首次计算 24 月;未变时命中 24 月且金额计算 0 次;内存纠正及恢复都仅重算 1 月、36 条课次。原始金额私存在 tmp,公开报告仅匿名计数与耗时。
+
+此最小月度方案仍需校验新鲜来源,不能声称消除了全历史数据库扫描。隔离测试强制原来源,不依赖尚在验证的来源索引。未变阶段原版约 1501 ms、候选约 1530 ms,来源 SQL 约 1464/1508 ms,单样本诊断没有证明总体提速。月摘要金额复用真实生效,但来源查询仍是主要瓶颈。
+
+随后对最终来源索引 + 月度摘要候选进行 10 对交替 AB/BA 隔离 API 测试(`teacher-api-isolated.json`):选择真实教师中 LessonRecord 最多者,1083 条记录、28 个月。全部 20 个响应的四个 gross 字段完全一致;首个候选计算 28 个月,此后 9 次都命中 28 个月且重算 0 月。服务 p75 从 1679 ms 降到 677 ms(减少约 59.7%),p50 从 1619 ms 降到 646 ms。这个收益来自组合后的来源筛选与月度复用,不能归功于月摘要单项。普通结果缓存每次清空,月摘要按新鲜输入签名验证后复用。clone 静态 digest readiness 为 true 桩,不是来源写入/触发器验收;服务计时不包含选人、建 clone、清缓存、认证和网络。测试 clone 与临时 Function 都已删除。
+
+可选 scoped source API 存在时使用其保守候选,再保留原 COALESCE 文本权限和预约谓词、按 ordinal 排序;不存在时依次使用带 authority guard 的 indexed 来源和原来源。scoped API 的数据库性能和完整 guard 等价由来源优化任务独立验收。
+
+发布材料生成:运行 `node perf/speed-round3-2026-11-10/teacher-monthly-release.mjs`。它只生成 tmp 中的 10 个函数 AST 补丁和公开 hash metadata,不部署。`teacherEarningsCalculator` 必须通过修改后的 `teacherEarningsSupport(operationsGatewayCode,payrollIntegrityCode)` 重新生成;不能只复制外层金额函数,否则 businessDate export 和完整闭包签名不完整。应用前核对 live baseline hash,迁移先创建受控派生表/函数,并确认 gateway 数据库角色权限。SQL/function/crypto 缺失时保持原金额计算降级。

+ 322 - 0
perf/speed-round3-2026-11-10/teacher-monthly-runtime.json

@@ -0,0 +1,322 @@
+{
+  "context": "Operator-bound read projection of an existing teacher, not teacher-login acceptance. Fresh authoritative original source reads (source index deliberately disabled). Only derived month summaries were written, in a temporary isolated schema removed in finally. Changed phase modifies one eligible lesson only in response memory; no business data or ledger writes. Times are diagnostic single samples, not a performance benchmark.",
+  "exactGrossAllPhases": true,
+  "isolatedSchemaRemoved": true,
+  "metrics": [
+    {
+      "runtime": {
+        "crypto": "object",
+        "subtle": true,
+        "TextEncoder": "function"
+      }
+    },
+    {
+      "phase": "cold",
+      "variant": "baseline",
+      "durationMs": 1528,
+      "calculateCalls": 0,
+      "monthlyReuse": null,
+      "queries": [
+        {
+          "kind": "policy",
+          "ms": 162
+        },
+        {
+          "kind": "policy",
+          "ms": 162
+        },
+        {
+          "kind": "policy",
+          "ms": 166
+        },
+        {
+          "kind": "review",
+          "ms": 167
+        },
+        {
+          "kind": "sources",
+          "ms": 1484
+        }
+      ]
+    },
+    {
+      "phase": "cold",
+      "variant": "candidate",
+      "durationMs": 1503,
+      "calculateCalls": 222,
+      "monthlyReuse": {
+        "available": true,
+        "matched": 0,
+        "computed": 24
+      },
+      "queries": [
+        {
+          "kind": "policy",
+          "ms": 1
+        },
+        {
+          "kind": "review",
+          "ms": 4
+        },
+        {
+          "kind": "policy",
+          "ms": 1
+        },
+        {
+          "kind": "policy",
+          "ms": 3
+        },
+        {
+          "kind": "policy",
+          "ms": 8
+        },
+        {
+          "kind": "sources",
+          "ms": 1449
+        },
+        {
+          "kind": "month-read",
+          "ms": 1
+        },
+        {
+          "kind": "month-read",
+          "ms": 3
+        },
+        {
+          "kind": "month-write",
+          "ms": 7
+        }
+      ]
+    },
+    {
+      "phase": "unchanged",
+      "variant": "baseline",
+      "durationMs": 1501,
+      "calculateCalls": 0,
+      "monthlyReuse": null,
+      "queries": [
+        {
+          "kind": "policy",
+          "ms": 2
+        },
+        {
+          "kind": "policy",
+          "ms": 2
+        },
+        {
+          "kind": "policy",
+          "ms": 13
+        },
+        {
+          "kind": "review",
+          "ms": 13
+        },
+        {
+          "kind": "sources",
+          "ms": 1464
+        }
+      ]
+    },
+    {
+      "phase": "unchanged",
+      "variant": "candidate",
+      "durationMs": 1530,
+      "calculateCalls": 0,
+      "monthlyReuse": {
+        "available": true,
+        "matched": 24,
+        "computed": 0
+      },
+      "queries": [
+        {
+          "kind": "review",
+          "ms": 3
+        },
+        {
+          "kind": "policy",
+          "ms": 1
+        },
+        {
+          "kind": "policy",
+          "ms": 2
+        },
+        {
+          "kind": "policy",
+          "ms": 6
+        },
+        {
+          "kind": "policy",
+          "ms": 11
+        },
+        {
+          "kind": "sources",
+          "ms": 1508
+        },
+        {
+          "kind": "month-read",
+          "ms": 2
+        },
+        {
+          "kind": "month-read",
+          "ms": 2
+        }
+      ]
+    },
+    {
+      "phase": "changed",
+      "variant": "baseline",
+      "durationMs": 1621,
+      "calculateCalls": 0,
+      "monthlyReuse": null,
+      "queries": [
+        {
+          "kind": "policy",
+          "ms": 6
+        },
+        {
+          "kind": "review",
+          "ms": 8
+        },
+        {
+          "kind": "policy",
+          "ms": 8
+        },
+        {
+          "kind": "policy",
+          "ms": 8
+        },
+        {
+          "kind": "sources",
+          "ms": 1545
+        }
+      ]
+    },
+    {
+      "phase": "changed",
+      "variant": "candidate",
+      "durationMs": 1526,
+      "calculateCalls": 36,
+      "monthlyReuse": {
+        "available": true,
+        "matched": 23,
+        "computed": 1
+      },
+      "queries": [
+        {
+          "kind": "policy",
+          "ms": 2
+        },
+        {
+          "kind": "review",
+          "ms": 4
+        },
+        {
+          "kind": "policy",
+          "ms": 1
+        },
+        {
+          "kind": "policy",
+          "ms": 3
+        },
+        {
+          "kind": "policy",
+          "ms": 8
+        },
+        {
+          "kind": "sources",
+          "ms": 1490
+        },
+        {
+          "kind": "month-read",
+          "ms": 1
+        },
+        {
+          "kind": "month-read",
+          "ms": 2
+        },
+        {
+          "kind": "month-write",
+          "ms": 8
+        }
+      ]
+    },
+    {
+      "phase": "restored",
+      "variant": "baseline",
+      "durationMs": 1598,
+      "calculateCalls": 0,
+      "monthlyReuse": null,
+      "queries": [
+        {
+          "kind": "policy",
+          "ms": 2
+        },
+        {
+          "kind": "review",
+          "ms": 4
+        },
+        {
+          "kind": "policy",
+          "ms": 4
+        },
+        {
+          "kind": "policy",
+          "ms": 5
+        },
+        {
+          "kind": "sources",
+          "ms": 1555
+        }
+      ]
+    },
+    {
+      "phase": "restored",
+      "variant": "candidate",
+      "durationMs": 1465,
+      "calculateCalls": 36,
+      "monthlyReuse": {
+        "available": true,
+        "matched": 23,
+        "computed": 1
+      },
+      "queries": [
+        {
+          "kind": "review",
+          "ms": 3
+        },
+        {
+          "kind": "policy",
+          "ms": 2
+        },
+        {
+          "kind": "policy",
+          "ms": 1
+        },
+        {
+          "kind": "policy",
+          "ms": 3
+        },
+        {
+          "kind": "policy",
+          "ms": 8
+        },
+        {
+          "kind": "sources",
+          "ms": 1441
+        },
+        {
+          "kind": "month-read",
+          "ms": 2
+        },
+        {
+          "kind": "month-read",
+          "ms": 2
+        },
+        {
+          "kind": "month-write",
+          "ms": 2
+        }
+      ]
+    }
+  ]
+}

+ 51 - 0
perf/speed-round3-2026-11-10/teacher-monthly-runtime.mjs

@@ -0,0 +1,51 @@
+import fs from 'node:fs';import ts from 'typescript';import assert from 'node:assert/strict';import {randomBytes} from 'node:crypto';import {withGateway} from './server-util.mjs';import {teacherEarningsSupport} from '../../scripts/teacher-earnings-transform.mjs';
+const read=p=>fs.readFileSync(p,'utf8'),privateDir='tmp/speed-round3-2026-11-10',outDir='perf/speed-round3-2026-11-10';
+const live=JSON.parse(read('/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-speed-round3/mobile-gateway.json')).code;
+function pick(code,names){const tree=ts.createSourceFile('gateway.js',code,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);return names.map(name=>{const node=tree.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name?.text===name);if(!node)throw Error('Missing '+name);return node.getText(tree);}).join('\n');}
+const company=live.match(/const DEFAULT_COMPANY_ID\s*=\s*['"]([^'"]+)/)[1],subject=JSON.parse(read(privateDir+'/mobile-subjects.json'))[0];
+const operations=read('scripts/deploy-admin-functions.mjs').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
+const candidate=teacherEarningsSupport(operations,read('scripts/cloud/payroll-integrity.js')).replace('calculate:payrollCalculatedLine','calculate:(...args)=>{calculateCalls++;return payrollCalculatedLine(...args)}')+'\n'+read('scripts/cloud/teacher-earnings.js');
+const baseline=pick(live,['teacherEarningsCalculator','teacherLessonEarnings','teacherIncomeSummary']).replace(/\bteacherEarningsCalculator\b/g,'baselineCalculator').replace(/\bteacherLessonEarnings\b/g,'baselineLessonEarnings').replace(/\bteacherIncomeSummary\b/g,'baselineIncomeSummary');
+const schema='speed3_teacher_'+randomBytes(6).toString('hex'),qualified='"'+schema+'".';
+const migration=read('scripts/sql/teacher-earned-months.sql').replace(/"TeacherEarnedMonth"/g,qualified+'"TeacherEarnedMonth"').replace(/\b(xs_teacher_earned_month_\w+)\b/g,qualified+'$1');
+const instrumentation=`let calculateCalls=0,lastMonthly=null,sqlMetrics=[],mutate=false,selectedKey=null,changedMonth=null;
+function scopedSql(sql){return sql.replace(/\\b(xs_teacher_earned_month_\\w+)\\b/g,${JSON.stringify(qualified)}+'$1');}
+function wrapDb(db){return new Proxy({},{get(_target,name){const target=db;if(name==='tx')return callback=>target.tx(tx=>callback(wrapDb(tx)));if(!['query','one','oneOrNone','none'].includes(name))return target[name];return async(sql,args)=>{
+ if(sql.includes('to_regprocedure')&&sql.includes('xs_payroll_source'))return {available:false}; // Force authoritative original sources; index is a separate experiment.
+ const start=Date.now(),value=await target[name](scopedSql(sql),args);sqlMetrics.push({kind:sql.includes('xs_payroll_sources')?'sources':sql.includes('xs_teacher_earned_month_read')?'month-read':sql.includes('xs_teacher_earned_month_write')?'month-write':sql.includes('ReviewRewardIncome')?'review':'policy',ms:Date.now()-start});
+ if(mutate&&sql.startsWith('SELECT s FROM xs_payroll_sources('))for(const row of value)if(row.s.standardLessonKey===selectedKey)row.s.teacherPaySnapshot=Number(row.s.teacherPaySnapshot||0)+1;
+ return value;
+ };}});}
+const MONTHLY_DB=wrapDb(Psql);`;
+const source=instrumentation+'\n'+(baseline+'\n'+candidate+'\n'+pick(live,['reviewIncomeTotal'])).replace(/\bPsql\b/g,'MONTHLY_DB')+`
+const monthlyActual=teacherMonthlyLessonEarnings;teacherMonthlyLessonEarnings=async(...args)=>{const result=await monthlyActual(...args);lastMonthly=result.monthlyReuse;return result;};teacherMonthlyLessonEarnings.toString=()=>monthlyActual.toString();
+async function run(input){
+ if(input.phase==='setup'){await Psql.none(${JSON.stringify('CREATE SCHEMA '+qualified.slice(0,-1)+';'+migration)});return {crypto:typeof crypto,subtle:!!crypto.subtle,TextEncoder:typeof TextEncoder};}
+ if(input.phase==='cleanup'){await Psql.none(${JSON.stringify('DROP SCHEMA '+qualified.slice(0,-1)+' CASCADE')});return {removed:true};}
+ const current=await new Parse.Query('_User').get(${JSON.stringify(subject.objectId)},{useMasterKey:true});
+ if(Number(current.get('legacyUserId'))!==${JSON.stringify(Number(subject.legacyUserId))}||Number(current.get('legacyGroupId'))!==${JSON.stringify(Number(subject.legacyGroupId))})throw Error('Existing teacher identity changed');
+ const co=${JSON.stringify(company)},coach=${JSON.stringify(Number(subject.legacyUserId))};
+ mutate=false;selectedKey=null;changedMonth=null;
+ if(input.phase==='changed'){
+  const original=await baselineLessonEarnings(co,coach),line=original.lines.find(l=>!l.exception&&l.originalMonth);if(!line)throw Error('No valid lesson to simulate correction');selectedKey=line.standardLessonKey;changedMonth=line.originalMonth;mutate=true;
+ }
+ sqlMetrics=[];calculateCalls=0;lastMonthly=null;const start=Date.now(),result=await (input.variant==='baseline'?baselineIncomeSummary:teacherIncomeSummary)(co,coach);
+ return {result,durationMs:Date.now()-start,calculateCalls,monthlyReuse:lastMonthly,changedMonth:mutate?changedMonth:null,queries:sqlMetrics};
+}`;
+const raw=[],metrics=[];
+await withGateway(source,async call=>{
+ let created=false;try{
+  const runtime=await call({phase:'setup'});created=true;assert.equal(runtime.subtle,true);assert.equal(runtime.TextEncoder,'function');metrics.push({runtime});
+  for(const phase of ['cold','unchanged','changed','restored']){
+   const pair={};for(const variant of ['baseline','candidate']){const value=await call({phase,variant});raw.push({phase,variant,...value});pair[variant]=value;metrics.push({phase,variant,durationMs:value.durationMs,calculateCalls:value.calculateCalls,monthlyReuse:value.monthlyReuse,queries:value.queries});}
+   assert.deepEqual(pair.candidate.result,pair.baseline.result,phase+' gross differs');
+   assert.equal(pair.candidate.monthlyReuse.available,true,phase+' monthly store unavailable');
+   if(phase==='cold')assert.ok(pair.candidate.monthlyReuse.computed>0);
+   if(phase==='unchanged'){assert.equal(pair.candidate.monthlyReuse.computed,0);assert.equal(pair.candidate.calculateCalls,0);}
+   if(phase==='changed'){assert.equal(pair.candidate.monthlyReuse.computed,1);assert.ok(pair.candidate.monthlyReuse.matched>0);}
+  }
+ }finally{if(created)assert.equal((await call({phase:'cleanup'})).removed,true);}
+});
+fs.writeFileSync(privateDir+'/teacher-monthly-runtime-raw.json',JSON.stringify(raw,null,2),{mode:0o600});
+const report={context:'Operator-bound read projection of an existing teacher, not teacher-login acceptance. Fresh authoritative original source reads (source index deliberately disabled). Only derived month summaries were written, in a temporary isolated schema removed in finally. Changed phase modifies one eligible lesson only in response memory; no business data or ledger writes. Times are diagnostic single samples, not a performance benchmark.',exactGrossAllPhases:true,isolatedSchemaRemoved:true,metrics};
+fs.writeFileSync(outDir+'/teacher-monthly-runtime.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2));

+ 55 - 0
perf/speed-round3-2026-11-10/verify-admin-live.mjs

@@ -0,0 +1,55 @@
+// Public hashes + authenticated read-only browser acceptance. No business mutations.
+import fs from 'node:fs';
+import assert from 'node:assert/strict';
+import {createHash} from 'node:crypto';
+import {chromium} from 'playwright';
+const env={};for(const line of fs.readFileSync('/Users/mac/.config/xiaoshu/legacy-sync-nightly.env','utf8').split('\n')){const m=line.match(/^(?:export\s+)?(XIAOSHU_[A-Z0-9_]+)=(.*)$/);if(m)env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');}
+const origin='https://admin.xiaoshu.pro',base='https://server.xiaoshu.pro',token=env.XIAOSHU_SYNC_OPERATOR_TOKEN;
+const manifest=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
+const sha=data=>createHash('sha256').update(data).digest('hex');
+async function get(url){const r=await fetch(url,{signal:AbortSignal.timeout(30000)});assert.ok(r.ok,'HTTP '+r.status);return r;}
+const current=await(await get(origin+'/deployment-version.json')).json();assert.equal(current.releaseId,manifest.releaseId);assert.equal(current.commit,manifest.commit);
+for(let i=0;i<manifest.files.length;i+=6)await Promise.all(manifest.files.slice(i,i+6).map(async f=>assert.equal(sha(Buffer.from(await(await get(origin+'/'+f.path)).arrayBuffer())),f.sha256,'Public hash mismatch: '+f.path)));
+const selectors={dashboard:'.overview-metrics',members:'app-operations-users tbody tr',stores:'app-operations-stores tbody tr',schedule:'.content-card[aria-busy="false"]',coaches:'app-operations-coaches tbody tr',payroll:'.payroll-summary-table tbody tr',lessons:'app-lesson-records tbody tr','learning-reports':'app-operations-learning-reports tbody tr','support-requests':'.request-list',vocabulary:'app-operations-vocabulary tbody tr','special-training':'.special-card'};
+const operations={dashboard:'ops/dashboard/summary',members:'ops/users/list',stores:'ops/stores/list',schedule:'ops/schedule/calendar',coaches:'ops/coaches/list',payroll:'ops/payroll/overview',lessons:'ops/lessons/list','learning-reports':'ops/learning-reports/list','support-requests':'ops/support-requests/list',vocabulary:'ops/vocabulary/tree','special-training':'ops/special-training/list'};
+const indexHash=manifest.files.find(f=>f.path==='index.html').sha256;
+for(const page of ['login',...Object.keys(selectors)])assert.equal(sha(Buffer.from(await(await get(origin+'/admin/'+page)).arrayBuffer())),indexHash,'Deep route: '+page);
+console.log(JSON.stringify({publicAssets:manifest.files.length,deepRoutes:12,releaseId:manifest.releaseId}));
+const meResponse=await fetch(base+'/parse/users/me',{headers:{'X-Parse-Application-Id':env.XIAOSHU_PARSE_APP_ID,'X-Parse-Session-Token':token}}),me=await meResponse.json();assert.ok(me.objectId,'Operator session invalid');
+const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}),pages=[];
+try{
+for(const [name,selector] of Object.entries(selectors)){
+ const ctx=await browser.newContext({viewport:{width:1440,height:1000}}),page=await ctx.newPage(),errors=[],records=[],jobs=[];
+ await ctx.addInitScript(auth=>localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth)),{objectId:me.objectId,sessionToken:token,username:'上线验收',roles:me.roles,company:me.company,operationsRole:'ops-manager',isSuperAdmin:true});
+ const allowed=new Set([...Object.values(operations),'ops/vocabulary/list','ops/live/versions','ops/payroll/list','ops/pay-rates/list','ops/teaching-rules/list','ops/payroll/coaches']);
+ await page.route('**/*',async route=>{const req=route.request();if(req.method()==='POST'&&req.url().includes('/api/functions/')){const op=req.postDataJSON()?.params?.operation;if(!allowed.has(op)){errors.push('Blocked unexpected operation');await route.abort();return;}}await route.continue();});
+ page.on('pageerror',()=>errors.push('Browser runtime error'));
+ const start=performance.now();
+ page.on('response',res=>{const req=res.request();if(req.method()!=='POST'||!req.url().includes('/api/functions/'))return;jobs.push((async()=>{try{const body=await res.json(),params=req.postDataJSON()?.params||{};records.push({operation:params.operation,displayOnly:params.displayOnly===true,summaryOnly:params.summaryOnly===true,lineCoachId:params.lineCoachId,success:res.ok()&&body.success===true,data:body.data});}catch{errors.push('Invalid API response');}})());});
+ const primary=page.waitForResponse(res=>res.request().method()==='POST'&&res.request().postDataJSON()?.params?.operation===operations[name],{timeout:60000});
+ const words=name==='vocabulary'?page.waitForResponse(res=>res.request().method()==='POST'&&res.request().postDataJSON()?.params?.operation==='ops/vocabulary/list',{timeout:60000}):null;
+ await page.goto(origin+'/admin/'+name,{waitUntil:'domcontentloaded'});await(await primary).finished();if(words)await(await words).finished();await page.locator(selector).first().waitFor({state:'visible',timeout:60000});await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));
+ const readyMs=Math.round(performance.now()-start);let verificationDisabled;
+ if(name==='payroll')await page.waitForFunction(()=>!document.querySelector('.payroll-source-row')?.textContent?.includes('正在核验结算资格'),null,{timeout:60000});
+ await Promise.all(jobs);assert.deepEqual(errors,[]);assert.ok(records.length&&records.every(r=>r.success),'Unsuccessful API: '+name);
+ const selected=records.filter(r=>r.operation===operations[name]);assert.ok(selected.length);
+ let coachDetailMs,exportVerified;
+ if(name==='payroll'){
+  const summary=selected.at(-1);assert.ok(summary.summaryOnly&&!summary.displayOnly);assert.notEqual(summary.data.sourceStatus.sync.state,'not_checked');assert.equal(summary.data.lines.length,0);
+  const exported=await(await fetch(base+'/api/functions/xiaoshu/ops/gateway-v3',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{operation:'ops/payroll/export',month:summary.data.month,summaryOnly:true,lineCoachId:summary.data.coachItems[0]?.coachId}}),signal:AbortSignal.timeout(60000)})).json();assert.equal(exported.success,true);assert.equal(exported.data.summary.payableAmount,summary.data.payableAmount);assert.equal(exported.data.coachItems.length,summary.data.coachItems.length);assert.ok(exported.data.lines.length>0);exportVerified=true;
+  verificationDisabled=await page.locator('.create-payroll-action').isDisabled();if(!summary.data.sourceStatus.sync.settlementReady)assert.equal(verificationDisabled,true);
+  const coachId=summary.data.coachItems[0]?.coachId;if(coachId){
+   const startDetail=performance.now(),response=page.waitForResponse(r=>r.request().method()==='POST'&&r.request().postDataJSON()?.params?.lineCoachId===coachId,{timeout:60000});
+   await page.locator('.view-action').first().click();const result=await(await response).json();assert.equal(result.success,true);assert.ok(result.data.lines.every(l=>l.coachId===coachId));assert.notEqual(result.data.sourceStatus.sync.state,'not_checked');
+   await page.waitForFunction(()=>!document.querySelector('.payroll-drawer')?.textContent?.includes('正在加载明细'),null,{timeout:60000});assert.equal(await page.locator('.payroll-drawer tbody tr').count(),Math.max(result.data.lines.length,1));coachDetailMs=Math.round(performance.now()-startDetail);
+   await page.locator('.payroll-drawer button[aria-label="关闭"]').click();await page.locator('.payroll-drawer').waitFor({state:'detached',timeout:5000});assert.equal(await page.locator('.payroll-drawer').count(),0);
+  }
+ }
+
+ let menuReturn;
+ if(['lessons','support-requests'].includes(name)){await page.locator('#admin-sidebar a[href="/admin/special-training"]').last().click();await page.locator('.special-card').first().waitFor({timeout:30000});await Promise.all(jobs);const count=records.filter(r=>r.operation===operations[name]).length,at=performance.now();await page.locator('#admin-sidebar a[href="/admin/'+name+'"]').last().click();await page.locator(selector).first().waitFor({timeout:30000});await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));await Promise.all(jobs);menuReturn={ms:Math.round(performance.now()-at),primaryRequests:records.filter(r=>r.operation===operations[name]).length-count};assert.equal(menuReturn.primaryRequests,0);}
+ const result={page:name,readyMs,passed:true,primaryRequests:selected.length,verificationDisabled,coachDetailMs,exportVerified,menuReturn};pages.push(result);console.log(JSON.stringify(result));await ctx.close();
+}
+}finally{await browser.close();}
+const receipt={at:new Date().toISOString(),origin,releaseId:manifest.releaseId,commit:manifest.commit,filesVerified:manifest.files.length,deepRoutesVerified:12,readOnly:true,measurement:'One production smoke sample per page; not comparable to controlled p75',pages};
+fs.writeFileSync('perf/speed-round3-2026-11-10/admin-release-verification.json',JSON.stringify(receipt,null,2)+'\n');

+ 10 - 0
perf/speed-round3-2026-11-10/verify-mobile-live.mjs

@@ -0,0 +1,10 @@
+import fs from 'node:fs';import assert from 'node:assert/strict';import {createHash} from 'node:crypto';import {chromium} from 'playwright';
+const origin='https://h5.xiaoshu.pro',manifest=JSON.parse(fs.readFileSync(process.argv[2],'utf8')),hash=x=>createHash('sha256').update(x).digest('hex');
+async function get(path){const response=await fetch(origin+'/'+path,{signal:AbortSignal.timeout(30000)});assert.ok(response.ok,'HTTP '+response.status+' '+path);return response;}
+const live=await(await get('deployment-version.json')).json();assert.equal(live.releaseId,manifest.releaseId);assert.equal(live.commit,manifest.commit);
+for(let i=0;i<manifest.files.length;i+=6)await Promise.all(manifest.files.slice(i,i+6).map(async file=>assert.equal(hash(Buffer.from(await(await get(file.path)).arrayBuffer())),file.sha256,'Public hash '+file.path)));
+const routes=['/pages/home/index','/pages/home/cart','/pages/home/shop','/pages/home/ucenter','/pages/his/his','/pages/stu/stu_calendar'];const indexHash=manifest.files.find(x=>x.path==='index.html').sha256;
+for(const route of routes)assert.equal(hash(Buffer.from(await(await get(route.slice(1))).arrayBuffer())),indexHash,'Deep route '+route);
+const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});let login;
+try{const context=await browser.newContext({viewport:{width:390,height:844}}),page=await context.newPage(),errors=[];page.on('pageerror',e=>errors.push(e.message));await page.goto(origin+routes[0],{waitUntil:'domcontentloaded'});await page.locator('input').first().waitFor({state:'visible',timeout:30000});assert.ok((await page.locator('body').innerText()).includes('登录'));assert.deepEqual(errors,[]);login={passed:true,unauthenticatedGate:true};await context.close();}finally{await browser.close();}
+const result={at:new Date().toISOString(),origin,releaseId:manifest.releaseId,commit:manifest.commit,publicAssetsVerified:manifest.files.length,deepRoutesVerified:routes.length,login,readOnly:true,limitations:'Public asset and unauthenticated login-gate acceptance. Role journeys were checked separately with anonymous fixtures; real-record API projections separately. No real teacher/student password login or native-device acceptance.'};fs.writeFileSync('perf/speed-round3-2026-11-10/mobile-release-verification.json',JSON.stringify(result,null,2));console.log(JSON.stringify(result));

+ 6 - 3
projects/xiaoshu-mobile/src/app/core/api.service.spec.ts

@@ -41,15 +41,18 @@ describe('ApiService', () => {
     expect(actual).toEqual([{ ID: 1 }]);
   });
 
-  it('treats lesson courses as a read and does not trigger appointment refresh loops', () => {
+  for (const action of ['app_lesson_courses', 'app_lesson_study', 'app_reading_source']) it(`treats ${action} as a read without invalidating or broadcasting unchanged teaching data`, () => {
     sessionToken = 'r:session-token';
+    const invalidate = spyOn(TestBed.inject(HomeDailyCacheService), 'invalidateCompanion');
     let changes = 0;
     api.dataChanges$.subscribe(() => changes++);
-    api.get('app_lesson_courses', { orderId: 901 }).subscribe();
+    api.get(action, { orderId: 901 }).subscribe();
     const request = http.expectOne(API_CONFIG.cloudFunctionUrl);
-    expect(request.request.body.params.action).toBe('app_lesson_courses');
+    expect(request.request.body.params.action).toBe(action);
     request.flush({ retcode: 0, result: { items: [] } });
     expect(changes).toBe(0);
+    expect(api.dataRevision).toBe(0);
+    expect(invalidate).not.toHaveBeenCalled();
   });
 
   for (const action of ['e_order_update_v2', 'app_lesson_complete', 'app_lesson_feedback_update']) {

+ 1 - 1
projects/xiaoshu-mobile/src/app/core/api.service.ts

@@ -91,7 +91,7 @@ export class ApiService {
 
   private isReadAction(action: string): boolean {
     return /(?:_list|_get|_detail|_overview|_tongji|_tj|_is)$/.test(action)
-      || ['reading/me','reading/query','reading/get','migration_status', 'app_update', 'node_list', 'node_get', 'content_get', 'vote_ask', 'vote_question', 'e_get_21list', 'app_live_versions', 'app_review_page', 'app_lesson_report', 'app_lesson_courses', 'app_exam_history'].includes(action);
+      || ['reading/me','reading/query','reading/get','migration_status', 'app_update', 'node_list', 'node_get', 'content_get', 'vote_ask', 'vote_question', 'e_get_21list', 'app_live_versions', 'app_review_page', 'app_lesson_report', 'app_lesson_courses', 'app_lesson_study', 'app_reading_source', 'app_exam_history'].includes(action);
   }
 
   private normalize<T>(input: unknown): ApiEnvelope<T> {

+ 1 - 0
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -515,6 +515,7 @@ describe('home learning modules', () => {
       const harness = await RouterTestingHarness.create();
       await harness.navigateByUrl('/pages/home/index');
       const first = { stats: count('e_order_tongji'), review: count('app_review_page'), balances: count('app_account_overview') };
+      if (groupId === 1) expect(get.calls.allArgs().find(([name]) => name === 'app_account_overview')?.[1]).toEqual(jasmine.objectContaining({ profileOnly: true, refresh: true }));
       document.dispatchEvent(new Event('visibilitychange'));
       await harness.navigateByUrl('/pages/member/starshop');
       await harness.navigateByUrl('/pages/home/index');

+ 33 - 0
projects/xiaoshu-mobile/src/app/core/teacher-today-review.service.spec.ts

@@ -57,6 +57,39 @@ describe('TeacherTodayReviewService', () => {
     expect(api.post.calls.mostRecent().args[1]).toEqual(jasmine.objectContaining({ refresh: true }));
   });
 
+  it('uses the server day for every remaining page across a local midnight mismatch', async () => {
+    const serverDay = '2099-01-02';
+    api.post.and.callFake((_action, params) => of({ result: {
+      ...page([task(Number(params?.['page']), serverDay)], 3), serverToday: serverDay,
+    } }) as never);
+    const result = await firstValueFrom(service.load());
+    expect(api.post.calls.allArgs().slice(1).map(([,params]) => params?.['date'])).toEqual([serverDay, serverDay]);
+    expect(result.items.length).toBe(3);
+  });
+
+  it('bounds concurrent page reads and restores page order before publishing the complete list', async () => {
+    const responses = new Map<number, Subject<unknown>>();
+    api.post.and.callFake((_action, params) => {
+      const response = new Subject<unknown>();
+      responses.set(Number(params?.['page']), response);
+      return response as never;
+    });
+    let published = false;
+    const loaded = firstValueFrom(service.load()).then(result => { published = true; return result; });
+    responses.get(1)!.next({ result: { ...page([task(1)], 6), serverToday: beijingToday() } });
+    expect([...responses.keys()]).toEqual([1, 2, 3, 4]);
+    responses.get(4)!.next({ result: page([task(4)], 6) });
+    expect([...responses.keys()]).toEqual([1, 2, 3, 4, 5]);
+    responses.get(3)!.next({ result: page([task(3)], 6) });
+    expect([...responses.keys()]).toEqual([1, 2, 3, 4, 5, 6]);
+    expect(published).toBeFalse();
+    for (const index of [6, 5, 2]) responses.get(index)!.next({ result: page([task(index)], 6) });
+    const result = await loaded;
+    expect(result.items.map(item => item['GeneralID'])).toEqual([1, 2, 3, 4, 5, 6]);
+    expect(result.summary.todayTotal).toBe(6);
+    expect(api.post.calls.allArgs().every(([,params]) => params?.['date'] === beijingToday())).toBeTrue();
+  });
+
   it('restores the same login daily cache after page reload', async () => {
     api.post.and.returnValue(of({ result: page([task(1)]) }) as never);
     await firstValueFrom(service.load());

+ 10 - 3
projects/xiaoshu-mobile/src/app/core/teacher-today-review.service.ts

@@ -1,5 +1,5 @@
 import { inject, Injectable } from '@angular/core';
-import { EMPTY, expand, finalize, map, Observable, of, reduce, shareReplay, take, tap } from 'rxjs';
+import { EMPTY, finalize, from, map, mergeMap, Observable, of, shareReplay, switchMap, take, tap, toArray } from 'rxjs';
 import { antiForgetCompleted, antiForgetDate } from './anti-forget';
 import { ApiService } from './api.service';
 import { ReviewPage } from './review-page';
@@ -33,8 +33,15 @@ export class TeacherTodayReviewService {
       ...(force && page === 1 ? { refresh: true } : {}),
     }).pipe(take(1), map(({ result }) => ({ result, page })));
     const request = fetchPage(1, date).pipe(
-      expand(({ result, page }) => key === this.ensureIdentity() && page < result.page.pageCount ? fetchPage(page + 1, result.serverToday || date) : EMPTY),
-      reduce((all, { result }) => all ? { ...all, items: [...all.items, ...result.items] } : result, null as ReviewPage | null),
+      switchMap(first => {
+        const remaining = Array.from({ length: Math.max(0, first.result.page.pageCount - 1) }, (_, index) => index + 2);
+        return from(remaining).pipe(
+          // Keep the first page's date and ordering, with at most three reads in flight.
+          mergeMap(page => key === this.ensureIdentity() ? fetchPage(page, first.result.serverToday || date) : EMPTY, 3),
+          toArray(),
+          map(pages => ({ ...first.result, items: [first, ...pages.sort((left, right) => left.page - right.page)].flatMap(({ result }) => result.items) })),
+        );
+      }),
       map((result) => {
         if (!result) throw new Error('今日复习数据为空');
         const serverToday = result.serverToday || date;

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/home/home-page.component.ts

@@ -202,7 +202,7 @@ export class HomePageComponent implements OnInit, OnDestroy {
   private loadBalances(uid: string | number, force = false): void {
     this.balanceLoading.set(true);
     this.requests.add(this.dailyCache.load('balances', () =>
-      this.api.get<{ profile: Record<string, unknown> }>('app_account_overview', { uid, refresh: true }).pipe(map(({ result }) => result.profile)), force,
+      this.api.get<{ profile: Record<string, unknown> }>('app_account_overview', { uid, refresh: true, profileOnly: true }).pipe(map(({ result }) => result.profile)), force,
     ).pipe(finalize(() => this.balanceLoading.set(false))).subscribe({
       next: ({ value }) => {
         this.balances.set(value);

+ 5 - 3
scripts/cloud/mobile-teaching.js

@@ -46,10 +46,12 @@ async function appReviewPage(input,current) {
   if(refreshCoachToday)assertLessonTenant(current);
   const legacyReviewSync=reviewTarget?await syncLegacyReviews(reviewTarget,current,input.refresh===true):
     refreshCoachToday?await syncLegacyCoachTodayReviews(uid,current,today):undefined;
-  const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"::text=$2'];
+  const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"=$2::text::numeric','m."'+(coach?'plid':'yhid')+'"::text=$2'];
+  // Keep the exact text identity match while letting the numeric ownership index narrow candidates.
   // Empty writing records are deliberate notes reviews; other empty sources remain hidden.
+  // OFFSET 0 keeps source checks correlated instead of evaluating every empty study in the tenant.
   const notesSource=notesReviewSourceSql();
-  clauses.push('NOT EXISTS (SELECT 1 FROM "CommonModel" sc JOIN "DailyStudyRecord" d ON d."company"=sc."company" AND d."id"=sc."itemId" WHERE sc."company"=m."company" AND sc."modelId"=56 AND sc."generalId"::text=m."xxjlid"::text AND COALESCE(d."learned",0)=0 AND COALESCE(NULLIF(BTRIM(d."xxqs"),\'\'),\'[]\')=\'[]\' AND NOT ('+notesSource+'))');
+  clauses.push('NOT EXISTS (SELECT 1 FROM "CommonModel" sc JOIN "DailyStudyRecord" d ON d."company"=sc."company" AND d."id"=sc."itemId" WHERE sc."company"=m."company" AND sc."modelId"=56 AND sc."generalId"::text=m."xxjlid"::text AND COALESCE(d."learned",0)=0 AND COALESCE(NULLIF(BTRIM(d."xxqs"),\'\'),\'[]\')=\'[]\' AND NOT ('+notesSource+') OFFSET 0)');
   const add=(sql,value)=>{args.push(value);clauses.push(sql.replace('?', '$'+args.length));};
   const studentsPromise=coach&&includeMetadata? Psql.query('SELECT m."yhid"::text AS id,COALESCE(MAX(NULLIF(BTRIM(u."realName"),\'\')),MAX(NULLIF(BTRIM(u."nickname"),\'\')),MAX(NULLIF(BTRIM(u."legacyUserData"->>\'HoneyName\'),\'\')),MAX(NULLIF(BTRIM(u."username"),\'\')),\'学员 \'||m."yhid"::text) AS name,COUNT(*)::int AS "taskCount" FROM "MemoryPracticeRecord" m LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+clauses.join(' AND ')+' GROUP BY m."yhid"',[...args]):Promise.resolve([]);
   if(coach&&number(input.studentId))add('m."yhid"::text=?',String(input.studentId));
@@ -65,7 +67,7 @@ async function appReviewPage(input,current) {
   if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
   const where=clauses.join(' AND '),countPromise=Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
   // Historical createdAt is the import time; the study model preserves the original learning time.
-  const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",COALESCE(n."nodeName",CASE WHEN '+notesSource+' THEN \'语法写作复习\' END) AS "kc_title",d."dqrq" AS "learned_date",dc."createTime" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",'+reviewStudentNameSql()+' AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN "CommonModel" c ON c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN "CommonModel" dc ON dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
+  const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",COALESCE(n."nodeName",CASE WHEN '+notesSource+' THEN \'语法写作复习\' END) AS "kc_title",d."dqrq" AS "learned_date",dc."createTime" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",'+reviewStudentNameSql()+' AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" OFFSET 0) c ON TRUE LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN LATERAL (SELECT * FROM "CommonModel" dc WHERE dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text OFFSET 0) dc ON TRUE LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
   const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
   const [students,summary,dates,count,rows,income,versions]=await Promise.all([studentsPromise,summaryPromise,datesPromise,countPromise,rowsPromise,coach&&input.includeIncome!==false?Psql.one('SELECT COALESCE(SUM(r."amount"),0) AS income FROM "MemoryPracticeRecord" m JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" WHERE '+where,args).then(row=>number(row.income)):0,appLiveVersionSnapshot(versionTarget,['review','account'],current)]);
   return {items:rows.map(row=>{const addon=row.__addon;delete row.__addon;return{...legacyAliases({...row,...addon},'CommonModel'),money:number(row.money),rewardStatus:row.rewardStatus||'pending'};}),page:mobilePageMeta(count.total,p),summary:{...summary,todayTotal:number(summary.today),todayCompleted:number(summary.todayCompleted),todayPending:number(summary.today)-number(summary.todayCompleted),overduePending:number(summary.overduePending),income,incomeScope:input.month||'all'},dates,students,legacyReviewSync,serverToday:today,...mobileSource(),...versions};

+ 24 - 5
scripts/cloud/payroll-integrity.js

@@ -15,10 +15,13 @@ function payrollTenant(context,input){requireRole(context,['ops-manager','ops-st
 async function payrollSql(sql,args){try{return await Psql.one(sql,args);}catch(error){fail(/does not exist/.test(text(error.message))?503:409,text(error.message).replace(/^.*ERROR:\s*/,''));}}
 function payrollIdentity(item){return text(item.standardLessonKey)||(number(item.legacyGeneralId)?'legacy:'+number(item.legacyGeneralId):'lesson:'+item.objectId);}
 const PAYROLL_SYNC_DATASETS=['appointments','lessons','memory-records'];
+function payrollSyncStatusFromCutoff(s,readAcceleration=false){
+ return {...(readAcceleration?{readSourceVersion:s.sourceVersion}:{}),state:s.approved?'cutoff_verified':'batch_verified',settlementReady:s.approved,cutoffVersion:s.version,monthClosed:s.monthClosed,lastSuccessAt:s.cutoffAt||'',lagSeconds:s.cutoffAt?Math.max(0,Math.floor((Date.now()-new Date(s.cutoffAt).getTime())/1000)):null,datasets:s.datasets,message:s.approved?(s.monthClosed?'该月份来源已核验;源数据变化后须重新确认':'已核验截至同步时间的来源,可生成草稿;整月同步后才能审核'):'旧接口来源截止核验待完成:'+(!s.complete?'凭据缺失或业务编号不一致':s.pending+' 条计薪差异待核对')};
+}
 async function payrollSettlementSyncStatus(context,input,readAcceleration=false){
  const co=payrollTenant(context,input),month=text(input.month)||businessDate().slice(0,7);
  try {const s=(await payrollSql('SELECT '+(readAcceleration?'xs_payroll_cutoff_snapshot_fast':'xs_payroll_cutoff_snapshot')+'($1,$2) AS result',[co,month])).result;
- return {...(readAcceleration?{readSourceVersion:s.sourceVersion}:{}),state:s.approved?'cutoff_verified':'batch_verified',settlementReady:s.approved,cutoffVersion:s.version,monthClosed:s.monthClosed,lastSuccessAt:s.cutoffAt||'',lagSeconds:s.cutoffAt?Math.max(0,Math.floor((Date.now()-new Date(s.cutoffAt).getTime())/1000)):null,datasets:s.datasets,message:s.approved?(s.monthClosed?'该月份来源已核验;源数据变化后须重新确认':'已核验截至同步时间的来源,可生成草稿;整月同步后才能审核'):'旧接口来源截止核验待完成:'+(!s.complete?'凭据缺失或业务编号不一致':s.pending+' 条计薪差异待核对')};
+ return payrollSyncStatusFromCutoff(s,readAcceleration);
  }catch(error){return {state:'error',settlementReady:false,message:'工资来源核验暂不可用:'+text(error.message).slice(0,140),datasets:[]};}
 }
 async function payrollCutoff(context,input,payload,confirm=false){
@@ -27,14 +30,28 @@ async function payrollCutoff(context,input,payload,confirm=false){
  return(await payrollSql('SELECT xs_payroll_cutoff_snapshot($1,$2) AS result',[co,month])).result;
 }
 
+async function payrollFreshReadSnapshot(co,month,forDashboard,includeScopeRows){
+ if(typeof Psql.tx!=='function')return null;
+ try{return await Psql.tx(async db=>{
+  // Legacy cutoff readers are VOLATILE; the transaction also pins their statements.
+  await db.none('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
+  const row=await db.one('SELECT xs_payroll_read_snapshot($1,$2,$3,$4) AS result',[co,month,forDashboard,includeScopeRows]);
+  return row.result||null;
+ });}catch{return null;} // Missing helper, authority drift or unavailable snapshot takes the original path.
+}
+async function payrollReadUsers(co){
+ try{return(await Psql.query('SELECT xs_payroll_read_users($1) AS row',[co])).map(item=>item.row);}
+ catch{return Psql.query('SELECT * FROM "_User" WHERE "company"=$1',[co]);}
+}
 async function payrollCanonicalSnapshot(context,input,options={}) {
   const co=payrollTenant(context,input);
   // A display-only estimate cannot authorize settlement. Financial reads and all
   // commands keep the full source version and cutoff checks by default.
   const forDashboard=options.forDashboard===true,readAcceleration=options.readAcceleration===true;
-  const [snapshot,syncResult]=await Promise.all([payrollSql('SELECT COALESCE(jsonb_agg(s),\'[]\'::jsonb) AS rows,'+((forDashboard||readAcceleration||options.omitSourceVersion===true)?"''::text":(readAcceleration?'xs_payroll_source_version_fast($1)':'xs_payroll_source_version($1)'))+' AS version FROM '+(readAcceleration?'xs_payroll_sources_cached':'xs_payroll_sources')+'($1) s',[co]),forDashboard?Promise.resolve({state:'not_checked',settlementReady:false,datasets:[],message:'金额已按实时完课记录计算,结算资格仍需完整核验'}):payrollSettlementSyncStatus(context,input,readAcceleration)]);
+  const fresh=readAcceleration&&options.readMonth?await payrollFreshReadSnapshot(co,options.readMonth,forDashboard,Boolean(context.storeId)):null;
+  const [snapshot,syncResult]=fresh?[fresh,forDashboard?{state:'not_checked',settlementReady:false,datasets:[],message:'金额已按实时完课记录计算,结算资格仍需完整核验'}:payrollSyncStatusFromCutoff(fresh.cutoff,true)]:await Promise.all([payrollSql('SELECT COALESCE(jsonb_agg(s),\'[]\'::jsonb) AS rows,'+((forDashboard||readAcceleration||options.omitSourceVersion===true)?"''::text":(readAcceleration?'xs_payroll_source_version_fast($1)':'xs_payroll_source_version($1)'))+' AS version FROM '+(readAcceleration?'xs_payroll_sources_cached':'xs_payroll_sources')+'($1) s',[co]),forDashboard?Promise.resolve({state:'not_checked',settlementReady:false,datasets:[],message:'金额已按实时完课记录计算,结算资格仍需完整核验'}):payrollSettlementSyncStatus(context,input,readAcceleration)]);
   const {readSourceVersion,...sync}=syncResult;
-  const users=await Psql.query('SELECT * FROM "_User" WHERE "company"=$1',[co]);
+  const users=readAcceleration?await payrollReadUsers(co):await Psql.query('SELECT * FROM "_User" WHERE "company"=$1',[co]);
   const map=new Map(users.map(u=>[number(u.legacyUserId||legacyOf(u).UserID),u]));
   const items=(snapshot.rows||[]).map(row=>{
     const item=payrollSourceProjection(row,'cache'),coach=map.get(item.coachId),student=map.get(item.studentId),storeId=item.storeId||number(coach&&legacyOf(coach).ParentUserID),store=map.get(storeId);
@@ -43,7 +60,9 @@ async function payrollCanonicalSnapshot(context,input,options={}) {
       studentName:student?displayName(student):item.studentName,storeId,storeName:store?displayName(store):item.storeName||'未绑定门店',
       teacherPaySnapshot:row.teacherPaySnapshot==null?null:Number(row.teacherPaySnapshot)};
   });
-  const scopedItems=context.storeId?items.filter(i=>i.storeId===context.storeId):items;return {items:scopedItems,version:readAcceleration&&!forDashboard&&options.omitSourceVersion!==true?text(readSourceVersion):snapshot.version,sourceStatus:{available:true,mode:'parse',message:'新数据库有效完课记录(含历史补充)',total:scopedItems.length,refreshedAt:new Date().toISOString(),complete:sync.settlementReady,stale:!sync.settlementReady,cacheHit:false,sync}};
+  const scopedItems=context.storeId?items.filter(i=>i.storeId===context.storeId):items;
+  const total=fresh?(context.storeId?(fresh.scopeRows||[]).filter(row=>{const coach=map.get(number(row[0]||row[1]));return(number(row[2]||row[3])||number(coach&&legacyOf(coach).ParentUserID))===context.storeId;}).length:number(fresh.total)):scopedItems.length;
+  return {items:scopedItems,version:fresh&&options.omitSourceVersion===true?'':readAcceleration&&!forDashboard&&options.omitSourceVersion!==true?text(readSourceVersion):snapshot.version,sourceStatus:{available:true,mode:'parse',message:'新数据库有效完课记录(含历史补充)',total,refreshedAt:new Date().toISOString(),complete:sync.settlementReady,stale:!sync.settlementReady,cacheHit:false,sync}};
 }
 async function payrollSourceRows(context,input,month,requireLive=false){const data=await payrollCanonicalSnapshot(context,input);return {...data,items:data.items.filter(i=>payrollBusinessDate(i.lessonAt).slice(0,7)===month)};}
 function payrollCalculatedLine(item,rates,settlementMonth,rules,mappings){
@@ -64,7 +83,7 @@ async function payrollPreviewData(context,input,month,includeUsed=false,requireL
 async function payrollPreviewUncached(context,input,month,includeUsed=false,requireLive=false,options={}){
  if(!/^\d{4}-(0[1-9]|1[0-2])$/.test(month))fail(400,'工资月份无效');
  if(context.storeId)input={...input,storeId:context.storeId};
- const co=payrollTenant(context,input),[source,rates,rules,mappings,used]=await Promise.all([payrollCanonicalSnapshot(context,{...input,month},{forDashboard:options.forDashboard===true&&includeUsed&&!requireLive,omitSourceVersion:options.omitSourceVersion===true&&includeUsed&&!requireLive,readAcceleration:options.readAcceleration===true&&includeUsed&&!requireLive&&!input.targetId}),payRateRows(context,input),teachingRuleRows(context,input),courseTeachingRows(context,input),Psql.query('SELECT "standardLessonKey","batchId","excluded" FROM "PayrollLine" WHERE "company"=$1 AND "kind"=\'lesson\' AND COALESCE("excluded",false)=false',[co])]);
+ const co=payrollTenant(context,input),[source,rates,rules,mappings,used]=await Promise.all([payrollCanonicalSnapshot(context,{...input,month},{forDashboard:options.forDashboard===true&&includeUsed&&!requireLive,omitSourceVersion:options.omitSourceVersion===true&&includeUsed&&!requireLive,readAcceleration:options.readAcceleration===true&&includeUsed&&!requireLive&&!input.targetId,readMonth:includeUsed&&!requireLive&&!input.targetId?month:undefined}),payRateRows(context,input),teachingRuleRows(context,input),courseTeachingRows(context,input),Psql.query('SELECT "standardLessonKey","batchId","excluded" FROM "PayrollLine" WHERE "company"=$1 AND "kind"=\'lesson\' AND COALESCE("excluded",false)=false',[co])]);
  const other=new Set(used.filter(r=>r.batchId!==input.targetId).map(r=>r.standardLessonKey)),mappingMap=new Map(mappings.filter(r=>r.confirmed).map(r=>[r.courseId,r]));
  const previous=month.endsWith('-01')?(Number(month.slice(0,4))-1)+'-12':month.slice(0,5)+String(Number(month.slice(5))-1).padStart(2,'0');
  const allowLate=!includeUsed&&Boolean((await Psql.one('SELECT EXISTS(SELECT 1 FROM "PayrollBatch" WHERE "company"=$1 AND "month"=$2 AND COALESCE("scopeStoreId",0)=$3 AND "status" IN (\'reviewed\',\'paid\')) AS value',[co,previous,context.storeId||0])).value);

+ 66 - 10
scripts/cloud/teacher-earnings.js

@@ -1,12 +1,23 @@
 // Read-only teacher view of the SAME canonical sources/calculator as operations payroll.
-async function teacherLessonEarnings(companyId,coachId,appointmentIds=null) {
-  const calc=teacherEarningsCalculator();
+async function teacherEarningsSourceRows(db,companyId,coachId,appointmentIds){
+ const available=await db.one("SELECT to_regprocedure('xs_payroll_source_entries_scoped(text,text,text,text[],boolean)') IS NOT NULL AS scoped,to_regprocedure('xs_payroll_sources_indexed(text)') IS NOT NULL AS available");
+ // The source selector is deliberately conservative; preserve exact ownership/appointment predicates here.
+ if(available.scoped)return db.query('SELECT row_data AS s FROM xs_payroll_source_entries_scoped($1,NULL,$2,$3,false) WHERE row_data IS NOT NULL AND COALESCE(row_data->>\'coachId\',row_data->>\'jsmz\')=$2 AND ($3::text[] IS NULL OR COALESCE(row_data->>\'appointmentGeneralId\',row_data->>\'yyds\')=ANY($3)) ORDER BY ordinal',[companyId,String(coachId),appointmentIds]);
+ const source=available.available?'xs_payroll_sources_indexed':'xs_payroll_sources';
+ return db.query('SELECT s FROM '+source+'($1) s WHERE COALESCE(s->>\'coachId\',s->>\'jsmz\')=$2 AND ($3::text[] IS NULL OR COALESCE(s->>\'appointmentGeneralId\',s->>\'yyds\')=ANY($3))',[companyId,String(coachId),appointmentIds]);
+}
+async function teacherEarningsInputs(companyId,coachId,appointmentIds=null,db=Psql) {
+  if(db===Psql&&typeof Psql.tx==='function')return Psql.tx(async tx=>{await tx.none('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');return teacherEarningsInputs(companyId,coachId,appointmentIds,tx);});
   const [sources,storedRates,storedRules,mappings]=await Promise.all([
-    Psql.query('SELECT s FROM xs_payroll_sources($1) s WHERE COALESCE(s->>\'coachId\',s->>\'jsmz\')=$2 AND ($3::text[] IS NULL OR COALESCE(s->>\'appointmentGeneralId\',s->>\'yyds\')=ANY($3))',[companyId,String(coachId),appointmentIds]),
-    Psql.query('SELECT * FROM "CoachPayRate" WHERE company=$1 ORDER BY "createdAt" DESC,"objectId" DESC',[companyId]),
-    Psql.query('SELECT * FROM "TeachingCompensationRule" WHERE company=$1 ORDER BY revision DESC,"createdAt" DESC,"objectId"',[companyId]),
-    Psql.query('WITH RECURSIVE ancestors AS (SELECT n."nodeId" AS course_id,n."nodeId",n."parentId",n."nodeName",0 AS depth FROM "Node" n WHERE company=$1 UNION ALL SELECT a.course_id,n."nodeId",n."parentId",n."nodeName",a.depth+1 FROM ancestors a JOIN "Node" n ON n.company=$1 AND n."nodeId"=a."parentId" WHERE a.depth<12) SELECT p.*,n."nodeName" AS "courseName",array_agg(a."nodeName" ORDER BY a.depth) AS ancestor_names FROM "CourseTeachingProfile" p LEFT JOIN "Node" n ON n.company=p.company AND n."nodeId"=p."courseId" LEFT JOIN ancestors a ON a.course_id=p."courseId" WHERE p.company=$1 GROUP BY p."objectId",n."nodeName"',[companyId])
+    teacherEarningsSourceRows(db,companyId,coachId,appointmentIds),
+    db.query('SELECT * FROM "CoachPayRate" WHERE company=$1 ORDER BY "createdAt" DESC,"objectId" DESC',[companyId]),
+    db.query('SELECT * FROM "TeachingCompensationRule" WHERE company=$1 ORDER BY revision DESC,"createdAt" DESC,"objectId"',[companyId]),
+    db.query('WITH RECURSIVE ancestors AS (SELECT n."nodeId" AS course_id,n."nodeId",n."parentId",n."nodeName",0 AS depth FROM "Node" n WHERE company=$1 UNION ALL SELECT a.course_id,n."nodeId",n."parentId",n."nodeName",a.depth+1 FROM ancestors a JOIN "Node" n ON n.company=$1 AND n."nodeId"=a."parentId" WHERE a.depth<12) SELECT p.*,n."nodeName" AS "courseName",array_agg(a."nodeName" ORDER BY a.depth) AS ancestor_names FROM "CourseTeachingProfile" p LEFT JOIN "Node" n ON n.company=p.company AND n."nodeId"=p."courseId" LEFT JOIN ancestors a ON a.course_id=p."courseId" WHERE p.company=$1 GROUP BY p."objectId",n."nodeName"',[companyId])
   ]);
+  return {sources,storedRates,storedRules,mappings};
+}
+function teacherPrepareEarnings(input,calc){
+  const {sources,storedRates,storedRules,mappings}=input;
   const rateKeys=new Set(),rates=[];
   for(const row of storedRates){const key=row.classType+':'+row.effectiveFrom;if(rateKeys.has(key))continue;rateKeys.add(key);rates.push({...row,classType:Number(row.classType),amount:Number(row.amount),durationHours:Number(row.durationHours),active:row.active!==false,isDefault:false});}
   rates.push(...calc.defaultRates().filter(r=>!rateKeys.has(r.classType+':'+r.effectiveFrom)));
@@ -15,12 +26,57 @@ async function teacherLessonEarnings(companyId,coachId,appointmentIds=null) {
   rules.push(...calc.defaultRules().filter(r=>!ruleKeys.has([r.categoryKey,r.durationMinutes,r.deliveryMode,r.effectiveFrom].join(':'))));
   const mappingMap=new Map(mappings.filter(r=>calc.category(r)).map(r=>[Number(r.courseId),r]));
   const unique=new Map();
-  for(const {s:raw} of sources){const row={...calc.project(raw,'cache'),standardLessonKey:raw.standardLessonKey,commonStatus:Number(raw.commonStatus??99),teacherPaySnapshot:raw.teacherPaySnapshot==null?null:Number(raw.teacherPaySnapshot)};if(row.commonStatus===-2)continue;if(unique.has(row.standardLessonKey))unique.get(row.standardLessonKey).duplicate=true;else unique.set(row.standardLessonKey,row);}
-  const lines=[...unique.values()].map(r=>calc.calculate(r,rates,'',rules,mappingMap));
-  return {lines,lessonIncome:lines.reduce((sum,l)=>sum+Math.round(l.amount*100),0)/100,pendingPayCount:lines.filter(l=>l.exception).length};
+  for(const {s:raw} of sources){const row={...calc.project(raw,'cache'),standardLessonKey:raw.standardLessonKey,commonStatus:Number(raw.commonStatus??99),teacherPaySnapshot:raw.teacherPaySnapshot==null?null:Number(raw.teacherPaySnapshot)};if(row.commonStatus===-2)continue;if(unique.has(row.standardLessonKey)){const group=unique.get(row.standardLessonKey);group.item.duplicate=true;group.raw.push(raw);}else unique.set(row.standardLessonKey,{item:row,raw:[raw]});}
+  return {calc,rates,rules,mappingMap,groups:[...unique.values()]};
+}
+function teacherCalculatePreparedEarnings(prepared){
+ const {calc,rates,rules,mappingMap,groups}=prepared,lines=groups.map(group=>calc.calculate(group.item,rates,'',rules,mappingMap));
+ return {lines,lessonIncome:lines.reduce((sum,l)=>sum+Math.round(l.amount*100),0)/100,pendingPayCount:lines.filter(l=>l.exception).length};
+}
+async function teacherLessonEarnings(companyId,coachId,appointmentIds=null){
+ return teacherCalculatePreparedEarnings(teacherPrepareEarnings(await teacherEarningsInputs(companyId,coachId,appointmentIds),teacherEarningsCalculator()));
 }
+async function teacherEarningsFingerprint(value){
+ if(typeof crypto==='undefined'||!crypto.subtle||typeof TextEncoder==='undefined')return null;
+ const hash=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(JSON.stringify(value)));
+ return Array.from(new Uint8Array(hash),byte=>byte.toString(16).padStart(2,'0')).join('');
+}
+async function teacherMonthlyEarningsInputs(companyId,coachId){
+ // Read derived generations in the SAME snapshot as their fresh source inputs.
+ // Otherwise an old reader can observe a newer summary and CAS-write old input over it.
+ if(typeof crypto==='undefined'||!crypto.subtle||typeof TextEncoder==='undefined'||typeof Psql.tx!=='function')return teacherEarningsInputs(companyId,coachId);
+ try{return await Psql.tx(async db=>{
+  await db.none('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
+  const input=await teacherEarningsInputs(companyId,coachId,null,db),available=await db.one("SELECT to_regprocedure('xs_teacher_earned_month_read(text,numeric,jsonb)') IS NOT NULL AS available");
+  const monthlyStore=available.available?(await db.one('SELECT xs_teacher_earned_month_read($1,$2,NULL) AS rows',[companyId,Number(coachId)])).rows||[]:[];
+  return {...input,monthlyStore,monthlyStoreAvailable:available.available};
+ });}catch{return teacherEarningsInputs(companyId,coachId);}
+}
+async function teacherMonthlyLessonEarnings(companyId,coachId){
+ const input=await teacherMonthlyEarningsInputs(companyId,coachId),prepared=teacherPrepareEarnings(input,teacherEarningsCalculator()),months=new Map();
+ // Preserve the original global representative/duplicate decision BEFORE month partitioning.
+ for(const group of prepared.groups){const month=prepared.calc.businessDate(group.item.lessonAt).slice(0,7)||'undated';if(!months.has(month))months.set(month,[]);months.get(month).push(group);}
+ if(!months.size)return{lessonIncome:0,pendingPayCount:0,monthlyReuse:{available:true,matched:0,computed:0}};
+ let dependencies;
+ try{dependencies=await teacherEarningsFingerprint({calculator:teacherEarningsCalculator.toString(),implementation:[teacherEarningsSourceRows,teacherEarningsInputs,teacherPrepareEarnings,teacherCalculatePreparedEarnings,teacherEarningsFingerprint,teacherMonthlyEarningsInputs,teacherMonthlyLessonEarnings].map(fn=>fn.toString()),rates:input.storedRates,rules:input.storedRules,mappings:input.mappings});}catch{dependencies=null;}
+ if(!dependencies)return{...teacherCalculatePreparedEarnings(prepared),monthlyReuse:{available:false,matched:0,computed:months.size}};
+ let requests;
+ try{requests=await Promise.all([...months].map(async([month,groups])=>({month,signature:await teacherEarningsFingerprint({dependencies,raw:groups.map(group=>group.raw)}),lineCount:groups.length})));}catch{requests=null;}
+ if(!requests||requests.some(request=>!request.signature))return{...teacherCalculatePreparedEarnings(prepared),monthlyReuse:{available:false,matched:0,computed:months.size}};
+ const stored=input.monthlyStore||[],available=input.monthlyStoreAvailable===true;
+ const saved=new Map(stored.map(row=>[row.month,row])),writes=[];let amountCents=0,pendingPayCount=0,matched=0,computed=0;
+ for(const request of requests){const previous=saved.get(request.month);let result;
+  if(previous?.signature===request.signature&&previous.lineCount===request.lineCount&&Number.isSafeInteger(previous.amountCents)&&Number.isSafeInteger(previous.pendingPayCount)){result=previous;matched++;}
+  else{const groups=months.get(request.month),lines=groups.map(group=>prepared.calc.calculate(group.item,prepared.rates,'',prepared.rules,prepared.mappingMap));result={amountCents:lines.reduce((sum,line)=>sum+Math.round(line.amount*100),0),pendingPayCount:lines.filter(line=>line.exception).length,lineCount:groups.length};computed++;writes.push({...request,...result,expectedSignature:previous?.signature??null});}
+  amountCents+=result.amountCents;pendingPayCount+=result.pendingPayCount;
+  if(!Number.isSafeInteger(result.amountCents)||!Number.isSafeInteger(amountCents))return{...teacherCalculatePreparedEarnings(prepared),monthlyReuse:{available:false,matched:0,computed:months.size}};
+ }
+ if(available&&writes.length)try{await Psql.one('SELECT xs_teacher_earned_month_write($1,$2,$3::jsonb) AS saved',[companyId,Number(coachId),JSON.stringify(writes)]);}catch{ /* Derived storage never changes the calculated response. */ }
+ return{lessonIncome:amountCents/100,pendingPayCount,monthlyReuse:{available,matched,computed}};
+}
+
 async function teacherIncomeSummary(companyId,coachId) {
-  const [lessons,reviewIncome]=await Promise.all([teacherLessonEarnings(companyId,coachId),reviewIncomeTotal(companyId,coachId)]);
+  const [lessons,reviewIncome]=await Promise.all([teacherMonthlyLessonEarnings(companyId,coachId),reviewIncomeTotal(companyId,coachId)]);
   return {lessonIncome:lessons.lessonIncome,reviewIncome,totalIncome:(Math.round(lessons.lessonIncome*100)+Math.round(reviewIncome*100))/100,pendingPayCount:lessons.pendingPayCount};
 }
 async function teacherSchedulePay(rows,coachId) {

+ 19 - 7
scripts/deploy-admin-functions.mjs

@@ -2925,7 +2925,7 @@ async function appLiveScopeVersion(scope,targetId,current){
     const learner=await legacyUserObject(targetId),ownerIds=learner?[learner.id,'_User$'+learner.id]:[];if(!ownerIds.length)return ':0';
     return appLiveVersion(await Psql.one('SELECT MAX(GREATEST(COALESCE(a."updatedAt",a."createdAt"),COALESCE(l."updatedAt",l."createdAt"))) AS changed_at,COUNT(DISTINCT a."objectId")::int+COUNT(DISTINCT l."objectId")::int AS total FROM "SurveyItem" a LEFT JOIN "SurveyLog" l ON CAST(l."surveyItem" AS text)=ANY(ARRAY[CAST(a."objectId" AS text),\'SurveyItem$\'||CAST(a."objectId" AS text)]) AND CAST(l."user" AS text)=ANY($1::text[]) WHERE CAST(a."user" AS text)=ANY($1::text[]) AND a."type"=\'reading\' AND COALESCE(a."isDeleted",FALSE)=FALSE',[ownerIds]));
   }
-  if(scope==='review')return appLiveVersion(await Psql.one('SELECT MAX(COALESCE("updatedAt","createdAt")) AS changed_at,COUNT(*)::int AS total FROM "MemoryPracticeRecord" WHERE "company"=$1 AND (CAST("yhid" AS text)=$2 OR CAST("plid" AS text)=$2)',[companyId,uid]));
+  if(scope==='review')return appLiveVersion(await Psql.one('SELECT MAX(COALESCE("updatedAt","createdAt")) AS changed_at,COUNT(*)::int AS total FROM "MemoryPracticeRecord" WHERE "company"=$1 AND (("yhid"=$2::text::numeric AND CAST("yhid" AS text)=$2) OR ("plid"=$2::text::numeric AND CAST("plid" AS text)=$2))',[companyId,uid]));
   if(scope==='account')return appLiveVersion(await Psql.one('SELECT MAX(changed_at) AS changed_at,SUM(total)::int AS total FROM (SELECT MAX(COALESCE("updatedAt","createdAt")) changed_at,COUNT(*)::int total FROM "_User" WHERE "company"=$1 AND "legacyUserId"=$2 UNION ALL SELECT MAX(COALESCE("updatedAt","createdAt")),COUNT(*)::int FROM "LessonRecord" WHERE "company"=$1 AND (CAST("xymz" AS text)=$3 OR CAST("jsmz" AS text)=$3) UNION ALL SELECT MAX(COALESCE("updatedAt","createdAt")),COUNT(*)::int FROM "CourseCreditReservation" WHERE "company"=$1 AND "studentId"=$2) versions',[companyId,targetId,uid]));
   return ':0';
 }
@@ -2979,18 +2979,29 @@ async function appCompanionOverview(input,current) {
   if(input.month&&/^\d{4}-\d{2}$/.test(input.month))add('LEFT('+date+',7)=?',input.month);
   const join=' FROM "CourseAppointment" a LEFT JOIN "CommonModel" c ON c."company"=a."company" AND c."modelId"=54 AND c."itemId"=a."id"';
   const summaryWhere=clauses.join(' AND '),summaryArgs=[...args];
-  const dates=await Psql.query('SELECT '+date+' AS date,COUNT(*) FILTER(WHERE a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2)::int finished,COUNT(*) FILTER(WHERE a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2)::int planned'+join+' WHERE '+summaryWhere+' GROUP BY '+date,summaryArgs);
-  const studentOptions=coach?await Psql.query('SELECT a."szyh" AS id,MAX('+companionStudentNameSql()+') AS name,COUNT(*)::int count'+join+' LEFT JOIN "_User" u ON u."company"=a."company" AND u."legacyUserId"::text=a."szyh" WHERE a."company"=$1 AND a."pl"=$2 AND COALESCE(c."status",99)<>-2 GROUP BY a."szyh" ORDER BY name',[DEFAULT_COMPANY_ID,String(uid)]):[];
+  const readDates=()=>Psql.query('SELECT '+date+' AS date,COUNT(*) FILTER(WHERE a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2)::int finished,COUNT(*) FILTER(WHERE a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2)::int planned'+join+' WHERE '+summaryWhere+' GROUP BY '+date,summaryArgs);
+  const readStudentOptions=()=>coach?Psql.query('SELECT a."szyh" AS id,MAX('+companionStudentNameSql()+') AS name,COUNT(*)::int count'+join+' LEFT JOIN "_User" u ON u."company"=a."company" AND u."legacyUserId"::text=a."szyh" WHERE a."company"=$1 AND a."pl"=$2 AND COALESCE(c."status",99)<>-2 GROUP BY a."szyh" ORDER BY name',[DEFAULT_COMPANY_ID,String(uid)]):[];
   if(input.dateStart)add(date+'>=?',String(input.dateStart));
   if(input.dateEnd)add(date+'<=?',String(input.dateEnd));
   if(input.filter==='finished')clauses.push('a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2');
   if(input.filter==='pending'){clauses.push('a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2');add(date+'<?',appOverviewToday());}
-  const where=clauses.join(' AND '),count=await Psql.one('SELECT COUNT(*)::int total'+join+' WHERE '+where,args);
-  const rows=await Psql.query('SELECT c."generalId",c."title",c."createTime",c."status" AS "commonStatus",a.*,n."nodeName" AS "courseTitle"'+join+' LEFT JOIN "Node" n ON n."company"=a."company" AND n."nodeId"::text=a."kcid" WHERE '+where+' ORDER BY a."yysj" '+(history?'DESC':'ASC')+',a."id" DESC LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
-  const courses=coach?[]:await Psql.query('SELECT c."generalId",c."title",b.*,b."objectId" AS "bindingObjectId",n."nodeName" AS "courseTitle" FROM "CourseBinding" b JOIN "CommonModel" c ON c."company"=b."company" AND c."modelId"=58 AND c."itemId"=b."id" LEFT JOIN "Node" n ON n."company"=b."company" AND n."nodeId"::text=b."kcid" WHERE b."company"=$1 AND b."yhid"=$2 AND b."enrollmentStatus"=\'active\' AND c."status"=99 AND COALESCE(n."zstatus",99)=99 ORDER BY b."id" DESC',[DEFAULT_COMPANY_ID,String(uid)]);
+  const where=clauses.join(' AND '),readCount=()=>Psql.one('SELECT COUNT(*)::int total'+join+' WHERE '+where,args);
+  const readRows=()=>Psql.query('SELECT c."generalId",c."title",c."createTime",c."status" AS "commonStatus",a.*,n."nodeName" AS "courseTitle"'+join+' LEFT JOIN "Node" n ON n."company"=a."company" AND n."nodeId"::text=a."kcid" WHERE '+where+' ORDER BY a."yysj" '+(history?'DESC':'ASC')+',a."id" DESC LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
+  const readCourses=()=>coach?[]:Psql.query('SELECT c."generalId",c."title",b.*,b."objectId" AS "bindingObjectId",n."nodeName" AS "courseTitle" FROM "CourseBinding" b JOIN "CommonModel" c ON c."company"=b."company" AND c."modelId"=58 AND c."itemId"=b."id" LEFT JOIN "Node" n ON n."company"=b."company" AND n."nodeId"::text=b."kcid" WHERE b."company"=$1 AND b."yhid"=$2 AND b."enrollmentStatus"=\'active\' AND c."status"=99 AND COALESCE(n."zstatus",99)=99 ORDER BY b."id" DESC',[DEFAULT_COMPANY_ID,String(uid)]);
+  const readAllRows=()=>coach&&history?Psql.query('SELECT c."generalId",c."status" AS "commonStatus",a.dszt'+join+' WHERE '+where,args):[];
+  // Each request reads fresh data; only independent reads overlap, with bounded database pressure.
+  const reads=[readDates,readStudentOptions,readCount,readRows,readCourses,readAllRows],results=[];
+  let nextRead=0,readError;
+  await Promise.all(Array.from({length:3},async()=>{
+    while(nextRead<reads.length&&!readError){
+      const index=nextRead++;
+      try{results[index]=await reads[index]();}catch(error){readError=error;}
+    }
+  }));
+  if(readError)throw readError;
+  const [dates,studentOptions,count,rows,courses,allRows]=results;
   let earningsSummary, scheduleRows=rows;
   if(coach&&history){
-    const allRows=await Psql.query('SELECT c."generalId",c."status" AS "commonStatus",a.dszt'+join+' WHERE '+where,args);
     const paidRows=await teacherSchedulePay(allRows,uid);
     earningsSummary={amount:paidRows.reduce((sum,row)=>sum+(Number(row.commonStatus)!==-2&&row.payStatus==='earned'?Math.round(Number(row.teacherPay)*100):0),0)/100,pendingCount:paidRows.filter(row=>Number(row.commonStatus)!==-2&&row.payStatus!=='earned').length};
     const payById=new Map(paidRows.map(row=>[String(row.generalId),row]));
@@ -3004,6 +3015,7 @@ async function appCompanionOverview(input,current) {
 }
 async function appAccountOverview(input,current) {
   const uid=await authorizeRequestedUser(current,input.uid),groupId=number(current.get('legacyGroupId')||legacyData(current).GroupID),profile={...legacyUser(current),...legacyUserAddon(current)},today=appOverviewToday(),todayCompact=today.replace(/-/g,'');
+  if(input.profileOnly===true)return{profile};
   if(groupId===3){const [lessons,schedules,reviews]=await Promise.all([
     Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "kclx"=\'1\')::int AS t30,COUNT(*) FILTER (WHERE "kclx"=\'2\')::int AS t60,COUNT(*) FILTER (WHERE "kclx"=\'3\')::int AS trial FROM "LessonRecord" WHERE "company"=$1 AND "jsmz"=$2',[DEFAULT_COMPANY_ID,String(uid)]),
     Psql.one('SELECT COUNT(*) FILTER (WHERE REPLACE(LEFT(COALESCE(NULLIF("bxrq",\'\'),"yysj"),10),\'-\',\'\')=$3)::int AS today,COUNT(*) FILTER (WHERE "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30)::int AS completed FROM "CourseAppointment" WHERE NOT EXISTS (SELECT 1 FROM "CommonModel" cancelled WHERE cancelled."company"="CourseAppointment"."company" AND cancelled."modelId"=54 AND cancelled."itemId"="CourseAppointment"."id" AND cancelled."status"=-2) AND "company"=$1 AND "pl"=$2',[DEFAULT_COMPANY_ID,String(uid),todayCompact]),

+ 62 - 0
scripts/prepare-mobile-speed3-web.mjs

@@ -0,0 +1,62 @@
+// Read-only origin backup and local web payload preparation. Never uploads.
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import {createHash} from 'node:crypto';
+import {execFileSync} from 'node:child_process';
+import {fileURLToPath} from 'node:url';
+import {manifestFor} from './release-admin-web.mjs';
+
+const repo=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
+const origin='https://xiaoshu-h5.obs.cn-south-1.myhuaweicloud.com/';
+const root=process.argv[2];
+if(!root||!path.isAbsolute(root))throw Error('Provide an absolute private mobile release directory');
+const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
+const safe=name=>typeof name==='string'&&name.length>0&&!name.startsWith('/')&&!name.includes('\\')&&!name.includes('%')&&!/[?#\r\n]/.test(name)&&name.split('/').every(part=>part&&part!=='.'&&part!=='..');
+async function get(name){
+ if(!safe(name))throw Error('Unsafe origin asset path');
+ const url=new URL(name,origin);url.searchParams.set('backup',Date.now().toString());
+ const response=await fetch(url,{headers:{'Cache-Control':'no-cache'},signal:AbortSignal.timeout(30000),redirect:'error'});
+ if(!response.ok)throw Error(`Origin HTTP ${response.status}: ${name}`);
+ return Buffer.from(await response.arrayBuffer());
+}
+await fs.mkdir(root,{recursive:true,mode:0o700});
+for(const directory of ['baseline','site']){
+ try{await fs.access(path.join(root,directory));throw Error('Refusing to replace existing '+directory);}catch(error){if(error.code!=='ENOENT')throw error;}
+}
+const stage=await fs.mkdtemp(path.join(root,'.prepare-'));
+const baseline=path.join(stage,'baseline'),site=path.join(stage,'site');
+await fs.mkdir(baseline,{mode:0o700});
+// A missing/invalid manifest fails closed. This release has a verified complete
+// manifest; older unmanifested releases need a separately reviewed asset crawl.
+const index=await get('index.html'),manifestBytes=await get('deployment-version.json');
+const remote=JSON.parse(manifestBytes.toString());
+if(remote.application!=='xiaoshu-mobile'||!Array.isArray(remote.files)||!remote.files.length)throw Error('Invalid mobile origin manifest');
+const seen=new Set();
+for(const file of remote.files){
+ if(!safe(file.path)||file.path==='deployment-version.json'||seen.has(file.path)||!/^\w{64}$/.test(file.sha256)||!/^[0-9a-f]+$/.test(file.sha256))throw Error('Invalid/duplicate manifest asset');
+ seen.add(file.path);
+}
+if(!seen.has('index.html'))throw Error('Origin manifest omits index');
+for(let offset=0;offset<remote.files.length;offset+=6){
+ await Promise.all(remote.files.slice(offset,offset+6).map(async file=>{
+  const data=file.path==='index.html'?index:await get(file.path);
+  if(sha(data)!==file.sha256)throw Error('Origin manifest hash mismatch: '+file.path);
+  const target=path.join(baseline,file.path);await fs.mkdir(path.dirname(target),{recursive:true});await fs.writeFile(target,data,{mode:0o600});
+ }));
+}
+await fs.writeFile(path.join(baseline,'deployment-version.json'),manifestBytes,{mode:0o600});
+const commit=execFileSync('git',['rev-parse','HEAD'],{cwd:repo,encoding:'utf8'}).trim();
+const releaseId='mobile-speed3-'+commit.slice(0,12)+'-'+new Date().toISOString().replace(/[-:.]/g,'');
+const build=path.join(repo,'dist/xiaoshu-mobile/browser');
+await fs.cp(build,site,{recursive:true});
+const manifest={...await manifestFor(site,releaseId,commit),application:'xiaoshu-mobile'};
+const sourceManifest=await manifestFor(build,releaseId,commit);
+if(JSON.stringify(sourceManifest.files)!==JSON.stringify(manifest.files))throw Error('Build changed while copying');
+await fs.writeFile(path.join(site,'deployment-version.json'),JSON.stringify(manifest,null,2)+'\n',{mode:0o600});
+// Fetch both mutable pointers again after all payload and backup work.
+if(sha(await get('index.html'))!==sha(index)||sha(await get('deployment-version.json'))!==sha(manifestBytes))throw Error('Production origin changed during backup; aborting preparation');
+const receipt={preparedAt:new Date().toISOString(),origin,baseline:{releaseId:remote.releaseId,commit:remote.commit,files:remote.files.length,indexHash:sha(index),manifestHash:sha(manifestBytes)},candidate:{releaseId,commit,commitProvisional:true,files:manifest.files.length,indexHash:manifest.files.find(file=>file.path==='index.html').sha256},build};
+await fs.rename(baseline,path.join(root,'baseline'));await fs.rename(site,path.join(root,'site'));
+await fs.rmdir(stage);
+await fs.writeFile(path.join(root,'preparation.json'),JSON.stringify(receipt,null,2)+'\n',{mode:0o600});
+console.log(JSON.stringify(receipt,null,2));

+ 89 - 0
scripts/speed-round3-patch.mjs

@@ -0,0 +1,89 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import vm from 'node:vm';
+import { createHash } from 'node:crypto';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import ts from 'typescript';
+import { teacherEarningsSupport } from './teacher-earnings-transform.mjs';
+
+// Builds reviewable private artifacts only. No credentials, network, SQL or deployment.
+export const MOBILE_HELPERS = Object.freeze(['appCompanionOverview', 'appAccountOverview', 'appLiveScopeVersion', 'appReviewPage']);
+export const MONTHLY_HELPERS = Object.freeze(['teacherEarningsCalculator', 'teacherEarningsSourceRows', 'teacherEarningsInputs', 'teacherPrepareEarnings', 'teacherCalculatePreparedEarnings', 'teacherLessonEarnings', 'teacherEarningsFingerprint', 'teacherMonthlyEarningsInputs', 'teacherMonthlyLessonEarnings', 'teacherIncomeSummary']);
+export const OPERATIONS_HELPERS = Object.freeze(['payrollSyncStatusFromCutoff', 'payrollSettlementSyncStatus', 'payrollFreshReadSnapshot', 'payrollReadUsers', 'payrollCanonicalSnapshot', 'payrollPreviewUncached']);
+const sha256 = value => createHash('sha256').update(value).digest('hex');
+const parse = code => ts.createSourceFile('gateway.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
+const named = (source, name) => source.statements.filter(node => ts.isFunctionDeclaration(node) && node.name?.text === name);
+
+export function patchFunctions(baseline, candidate, names) {
+  if (new Set(names).size !== names.length) throw Error('Duplicate helper in allowlist');
+  const before = parse(baseline), local = parse(candidate), replacements = [], additions = [], audit = [];
+  for (const name of names) {
+    const existing = named(before, name), desired = named(local, name);
+    if (existing.length > 1 || desired.length !== 1) throw Error(`Expected unique helper ${name}: baseline=${existing.length}, candidate=${desired.length}`);
+    const replacement = desired[0].getText(local), old = existing[0]?.getText(before);
+    if (old === replacement) { audit.push({ name, action: 'unchanged', hash: sha256(replacement) }); continue; }
+    if (existing.length) replacements.push({ start: existing[0].getStart(before), end: existing[0].end, replacement });
+    else additions.push(replacement);
+    audit.push({ name, action: existing.length ? 'replace' : 'add', beforeHash: old ? sha256(old) : null, afterHash: sha256(replacement) });
+  }
+  let code = baseline;
+  for (const edit of replacements.sort((a, b) => b.start - a.start)) code = code.slice(0, edit.start) + edit.replacement + code.slice(edit.end);
+  if (additions.length) code += '\n\n' + additions.join('\n\n') + '\n';
+  const after = parse(code), allowed = new Set(names);
+  const unselected = source => source.statements.filter(node => !(ts.isFunctionDeclaration(node) && allowed.has(node.name?.text))).map(node => node.getText(source));
+  const oldRest = JSON.stringify(unselected(before)), newRest = JSON.stringify(unselected(after));
+  if (oldRest !== newRest) throw Error('Patch changed a declaration or executable statement outside the helper allowlist');
+  for (const name of names) {
+    const target = named(after, name);
+    if (target.length !== 1 || target[0].getText(after) !== named(local, name)[0].getText(local)) throw Error(`Final helper mismatch: ${name}`);
+  }
+  new vm.Script(code, { filename: 'reviewable-gateway-candidate.js' });
+  return { code, audit: { baselineHash: sha256(baseline), candidateHash: sha256(code), unchangedStatementsHash: sha256(oldRest), helperAllowlist: [...names], functions: audit } };
+}
+
+export function localCandidates(root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')) {
+  const read = name => fs.readFileSync(path.join(root, name), 'utf8');
+  const deploy = read('scripts/deploy-admin-functions.mjs');
+  const template = name => {
+    const match = deploy.match(new RegExp('const ' + name + ' = String.raw`([\\s\\S]*?)\\n`;'));
+    if (!match) throw Error('Cannot find authoritative gateway template ' + name);
+    return match[1];
+  };
+  const operations = template('operationsGatewayCode'), integrity = read('scripts/cloud/payroll-integrity.js');
+  // Exactly the same generator and arguments as deploy-admin-functions.mjs.
+  const calculator = teacherEarningsSupport(operations, integrity);
+  return {
+    mobile: template('appGatewayCode') + '\n' + read('scripts/cloud/mobile-teaching.js') + '\n' + calculator + '\n' + read('scripts/cloud/teacher-earnings.js'),
+    operations: operations + '\n' + integrity,
+  };
+}
+
+function main(args) {
+  const values = new Map(); let monthly = false;
+  for (let index = 0; index < args.length; index++) {
+    const flag = args[index];
+    if (flag === '--monthly') { monthly = true; continue; }
+    if (!['--mobile-baseline', '--operations-baseline', '--output-dir'].includes(flag) || !args[index + 1] || args[index + 1].startsWith('--')) throw Error('Usage: node scripts/speed-round3-patch.mjs [--mobile-baseline private.json] [--operations-baseline private.json] --output-dir private-directory [--monthly]');
+    if (values.has(flag)) throw Error('Duplicate option ' + flag);
+    values.set(flag, args[++index]);
+  }
+  if (!values.has('--output-dir') || !values.has('--mobile-baseline') && !values.has('--operations-baseline')) throw Error('Provide explicit baseline file(s) and private --output-dir');
+  if (monthly && !values.has('--mobile-baseline')) throw Error('--monthly requires --mobile-baseline');
+  const candidates = localCandidates(), out = path.resolve(values.get('--output-dir')), manifest = { monthly, scopes: {} }, prepared = [];
+  for (const scope of ['mobile', 'operations']) {
+    const sourcePath = values.get(`--${scope}-baseline`); if (!sourcePath) continue;
+    const saved = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
+    if (typeof saved.code !== 'string' || !saved.code.trim()) throw Error(`${scope} baseline must be a saved Function object with nonempty code`);
+    const names = scope === 'mobile' ? [...MOBILE_HELPERS, ...(monthly ? MONTHLY_HELPERS : [])] : OPERATIONS_HELPERS;
+    const result = patchFunctions(saved.code, candidates[scope], names);
+    prepared.push({ filename: `${scope}-candidate.json`, value: { ...saved, code: result.code } });
+    manifest.scopes[scope] = result.audit;
+  }
+  // Validate every candidate before writing any file. Baseline files are never overwritten.
+  const baselinePaths = ['--mobile-baseline', '--operations-baseline'].map(flag => values.get(flag)).filter(Boolean).map(value => path.resolve(value));
+  for (const filename of [...prepared.map(item => item.filename), 'patch-audit.json']) if (baselinePaths.includes(path.join(out, filename))) throw Error('Output must not replace a baseline');
+  fs.mkdirSync(out, { recursive: true, mode: 0o700 });
+  for (const item of [...prepared, { filename: 'patch-audit.json', value: manifest }]) fs.writeFileSync(path.join(out, item.filename), JSON.stringify(item.value, null, 2) + '\n', { mode: 0o600 });
+  console.log(JSON.stringify({ outputDir: out, scopes: Object.fromEntries(Object.entries(manifest.scopes).map(([scope, audit]) => [scope, { baselineHash: audit.baselineHash, candidateHash: audit.candidateHash, functions: audit.functions.map(({ name, action }) => ({ name, action })) }])) }, null, 2));
+}
+if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) main(process.argv.slice(2));

+ 56 - 0
scripts/sql/payroll-cutoff-bulk.sql

@@ -0,0 +1,56 @@
+-- Parse legacy conflict payloads with the authoritative function's exception semantics.
+CREATE OR REPLACE FUNCTION xs_payroll_conflict_payload(value text) RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $$
+BEGIN RETURN value::jsonb;EXCEPTION WHEN others THEN RETURN '{}'::jsonb;END $$;
+
+-- Fresh set-based conflict calculation; no result cache and no financial writes.
+CREATE OR REPLACE FUNCTION xs_payroll_cutoff_conflicts_bulk(co text,mon text) RETURNS SETOF jsonb
+LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
+BEGIN
+ IF NOT xs_payroll_read_snapshot_authority_ready() THEN RETURN QUERY SELECT s FROM xs_payroll_cutoff_conflicts(co,mon) s;RETURN;END IF;
+ RETURN QUERY
+ WITH latest AS MATERIALIZED (
+  SELECT "runId" FROM "LegacyDirectRun" WHERE company=co AND status IN ('completed','completed-with-preserved-conflicts') ORDER BY "finishedAt" DESC LIMIT 1
+ ), roots AS MATERIALIZED (
+  SELECT f.*,c."modelId",c."itemId",c.title,c.subtitle,row_number() OVER(ORDER BY f."objectId") ord
+  FROM "LegacySyncConflict" f LEFT JOIN "CommonModel" c ON c.company=f.company AND c."generalId"::text=f."legacyKey"
+    AND c."modelId"=CASE f.dataset WHEN '54' THEN 54 WHEN '59' THEN 59 WHEN '60' THEN 60 END
+  WHERE f.company=co AND f.dataset IN ('54','59','60') AND NOT COALESCE(f.resolved,false)
+ ), joined AS MATERIALIZED (
+  SELECT r.*,CASE r.dataset WHEN '54' THEN to_jsonb(a) WHEN '59' THEN to_jsonb(l) ELSE to_jsonb(m) END item,
+   xs_payroll_conflict_payload(r.message) payload
+  FROM roots r
+  LEFT JOIN "CourseAppointment" a ON r.dataset='54' AND a.company=co AND a.id=r."itemId"
+  LEFT JOIN "LessonRecord" l ON r.dataset='59' AND l.company=co AND l.id=r."itemId"
+  LEFT JOIN "MemoryPracticeRecord" m ON r.dataset='60' AND m.company=co AND m.id=r."itemId"
+ ), ambiguous AS MATERIALIZED (
+  SELECT EXISTS(SELECT 1 FROM roots GROUP BY "objectId" HAVING count(*)>1)
+   OR EXISTS(SELECT 1 FROM joined GROUP BY ord HAVING count(*)>1) value
+ ), described AS MATERIALIZED (
+  SELECT j.*,CASE dataset WHEN '54' THEN item->>'yysj' WHEN '59' THEN COALESCE(NULLIF(item->>'lessonAt',''),NULLIF(item->>'plpjsj','')) ELSE COALESCE(NULLIF(item->>'wcsj',''),NULLIF(item->>'kywrq','')) END dt,
+   CASE WHEN jsonb_typeof(payload->'fields')='array' THEN payload->'fields' ELSE '[]'::jsonb END fields
+  FROM joined j WHERE NOT (SELECT value FROM ambiguous)
+ ), fingerprinted AS MATERIALIZED (
+  SELECT d.*,md5(jsonb_build_array(message,item,title,subtitle,(SELECT "runId" FROM latest))::text) fingerprint,
+   jsonb_array_length(fields)>0 AND NOT EXISTS(SELECT 1 FROM jsonb_array_elements_text(fields) f WHERE f NOT IN ('title','inputer')) cosmetic
+  FROM described d
+  WHERE xs_payroll_time(dt) IS NULL OR to_char(xs_payroll_time(dt) AT TIME ZONE 'Asia/Shanghai','YYYY-MM')<=mon
+ ), results AS (SELECT r.ord,jsonb_build_object('id',r."objectId",'dataset',r.dataset,'legacyKey',r."legacyKey",'student',r.title,'course',r.subtitle,'date',r.dt,'fields',r.fields,'reason',r.payload->>'reason','current',r.item,'fingerprint',r.fingerprint,'cosmetic',r.cosmetic,
+  'resolved',r.cosmetic OR EXISTS(SELECT 1 FROM "PayrollCutoffDecision" d WHERE d.company=co AND d.month=mon AND d."conflictId"=r."objectId" AND d.fingerprint=r.fingerprint))
+ AS value FROM fingerprinted r
+ UNION ALL SELECT c.ord,c.value FROM ambiguous guard
+ CROSS JOIN LATERAL xs_payroll_cutoff_conflicts(co,mon) WITH ORDINALITY c(value,ord) WHERE guard.value
+ ) SELECT value FROM results ORDER BY ord;
+END $$;
+
+-- Exact text membership before numeric duplicate counting, without the redundant
+-- DISTINCT materialization or a per-ID nested lookup. Local settings affect only
+-- this one fresh evidence query, not source readers or financial commands.
+CREATE OR REPLACE FUNCTION xs_payroll_evidence_matches(co text,model numeric,ids jsonb) RETURNS integer
+LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' SET enable_mergejoin=off SET enable_nestloop=off AS $$
+DECLARE tab text; matches integer;
+BEGIN
+ tab:=CASE model WHEN 54 THEN 'CourseAppointment' WHEN 59 THEN 'LessonRecord' WHEN 60 THEN 'MemoryPracticeRecord' END;
+ IF tab IS NULL THEN RAISE EXCEPTION 'Unsupported evidence model';END IF;
+ EXECUTE format('WITH local_rows AS MATERIALIZED (SELECT c."generalId" FROM "CommonModel" c JOIN %I a ON a.company=c.company AND a.id=c."itemId" WHERE c.company=$1 AND c."modelId"=$2) SELECT count(*)::int FROM (SELECT l."generalId" FROM local_rows l WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text($3) w WHERE w=l."generalId"::text) GROUP BY l."generalId" HAVING count(*)=1) x',tab) INTO matches USING co,model,ids;
+ RETURN matches;
+END $$;

+ 93 - 0
scripts/sql/payroll-read-snapshot.sql

@@ -0,0 +1,93 @@
+-- One fresh read snapshot; no result cache. Deploy after payroll-source-index and cutoff dependencies.
+-- Call inside REPEATABLE READ READ ONLY because downstream legacy read helpers may be VOLATILE.
+-- Capture the exact date parser used for conservative transport pruning. A later
+-- parser change fails closed rather than assuming it still agrees with JavaScript.
+DO $install$
+DECLARE expected text:=md5(pg_get_functiondef('xs_payroll_time(text)'::regprocedure));
+ conflicts_expected text:=md5(pg_get_functiondef('xs_payroll_cutoff_conflicts(text,text)'::regprocedure));
+BEGIN
+ IF to_regprocedure('xs_payroll_read_snapshot_authority_ready()') IS NOT NULL THEN RETURN; END IF;
+ EXECUTE format('CREATE OR REPLACE FUNCTION xs_payroll_read_snapshot_authority_ready() RETURNS boolean LANGUAGE sql STABLE AS %L',
+  'SELECT xs_payroll_authority_ready() AND md5(pg_get_functiondef(''xs_payroll_time(text)''::regprocedure))='||quote_literal(expected)||
+  ' AND md5(pg_get_functiondef(''xs_payroll_cutoff_conflicts(text,text)''::regprocedure))='||quote_literal(conflicts_expected));
+END $install$;
+
+CREATE OR REPLACE FUNCTION public.xs_payroll_cutoff_from_version(co text, mon text, provided_version text)
+ RETURNS jsonb
+ LANGUAGE plpgsql STABLE
+ SET jit TO 'off'
+ SET work_mem TO '64MB'
+ SET max_parallel_workers_per_gather TO '2'
+AS $function$
+DECLARE run "LegacyDirectRun"; ev record; table_name text; matches integer; complete boolean:=true; conflicts jsonb; version text; approval "PayrollCutoffApproval"; data_version text; source_version text; pending integer; datasets jsonb:='[]'; ready boolean;
+BEGIN
+ IF NOT xs_payroll_read_snapshot_authority_ready() THEN RETURN NULL;END IF;
+ IF mon !~ '^\d{4}-(0[1-9]|1[0-2])$' THEN RAISE EXCEPTION '工资月份无效'; END IF;
+ SELECT * INTO run FROM "LegacyDirectRun" WHERE company=co AND status IN ('completed','completed-with-preserved-conflicts') ORDER BY "finishedAt" DESC LIMIT 1;
+ IF NOT FOUND THEN complete:=false; END IF;
+ IF (SELECT count(*) FROM "LegacyDirectEvidence" WHERE company=co AND "runId"=run."runId" AND model IN (54,59,60))<>3 THEN complete:=false; END IF;
+ FOR ev IN SELECT * FROM "LegacyDirectEvidence" WHERE company=co AND "runId"=run."runId" AND model IN (54,59,60) LOOP
+  table_name:=CASE ev.model WHEN 54 THEN 'CourseAppointment' WHEN 59 THEN 'LessonRecord' ELSE 'MemoryPracticeRecord' END;
+  matches:=xs_payroll_evidence_matches(co,ev.model,ev.ids);
+  complete:=complete AND matches=ev."sourceCount" AND ev."sourceChecksum"=ev."targetChecksum" AND jsonb_array_length(ev.ids)=ev."sourceCount" AND ev."sourceChecksum"=md5((SELECT COALESCE(string_agg(v,E'\n' ORDER BY v::numeric),'') FROM jsonb_array_elements_text(ev.ids) v));
+  datasets:=datasets||jsonb_build_array(jsonb_build_object('model',ev.model,'source',ev."sourceCount",'matched',matches,'checksum',ev."sourceChecksum"));
+ END LOOP;
+ SELECT COALESCE(jsonb_agg(c ORDER BY c->>'id'),'[]') INTO conflicts FROM xs_payroll_cutoff_conflicts_bulk(co,mon) c;
+ SELECT count(*) INTO pending FROM jsonb_array_elements(conflicts) c WHERE NOT (c->>'resolved')::boolean;
+ -- Native row serialization covers every column without constructing a JSON object per row.
+ data_version:=xs_payroll_data_version(co);
+ source_version:=provided_version;
+ version:=md5(jsonb_build_array(data_version,source_version,run."runId",run."finishedAt",datasets,conflicts)::text);
+ SELECT * INTO approval FROM "PayrollCutoffApproval" WHERE company=co AND month=mon;
+ ready:=complete AND pending=0 AND approval.version=version AND NOT EXISTS(SELECT 1 FROM "LegacyDirectRun" WHERE company=co AND status='running');
+ RETURN jsonb_build_object('month',mon,'version',version,'sourceVersion',source_version,'runId',run."runId",'cutoffAt',run."finishedAt",'complete',complete,'pending',pending,'conflicts',conflicts,'datasets',datasets,'approved',COALESCE(ready,false),'monthClosed',COALESCE(run."finishedAt">=((mon||'-01')::date+interval '1 month') AT TIME ZONE 'Asia/Shanghai',false),'approval',to_jsonb(approval));
+END $function$;
+
+CREATE OR REPLACE FUNCTION xs_payroll_read_snapshot(co text, mon text, for_dashboard boolean DEFAULT false, include_scope_rows boolean DEFAULT false)
+RETURNS jsonb LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
+DECLARE result jsonb; cutoff jsonb;
+BEGIN
+ IF NOT xs_payroll_read_snapshot_authority_ready() THEN RETURN NULL; END IF;
+ IF mon !~ '^\d{4}-(0[1-9]|1[0-2])$' THEN RAISE EXCEPTION '工资月份无效'; END IF;
+ WITH sources AS MATERIALIZED (
+  SELECT row_data AS s,row_text,standard_key,scope_data,ordinal AS ord FROM xs_payroll_source_entries_scoped(co,mon,NULL,NULL,NOT for_dashboard)
+ ), identity_state AS MATERIALIZED (
+  SELECT EXISTS(SELECT 1 FROM sources GROUP BY standard_key HAVING count(*)>1) AS duplicates
+ ), source_version AS MATERIALIZED (
+  SELECT CASE WHEN for_dashboard THEN '' ELSE md5(
+   COALESCE((SELECT string_agg(row_text,'|' ORDER BY row_text) FROM sources),'')||
+   COALESCE((SELECT string_agg(to_jsonb(r)::text,'|' ORDER BY r."objectId") FROM "TeachingCompensationRule" r WHERE r."company"=co),'')||
+   COALESCE((SELECT string_agg(to_jsonb(p)::text,'|' ORDER BY p."objectId") FROM "CourseTeachingProfile" p WHERE p."company"=co),'')||
+   COALESCE((SELECT string_agg(to_jsonb(p)::text,'|' ORDER BY p."objectId") FROM "CoachPayRate" p WHERE p."company"=co),'')||
+   COALESCE((SELECT string_agg(to_jsonb(r)::text,'|' ORDER BY r."objectId") FROM "ReviewRewardIncome" r WHERE r."company"=co AND r."status"='earned'),'')) END AS value
+ ), candidates AS MATERIALIZED (
+  SELECT s,ord FROM sources WHERE s IS NOT NULL
+ ) SELECT jsonb_build_object(
+  'rows',COALESCE((SELECT jsonb_agg(s ORDER BY ord) FROM candidates),'[]'::jsonb),
+  'version',(SELECT value FROM source_version),
+  'total',(SELECT count(*) FROM sources),
+  'hasDuplicates',(SELECT duplicates FROM identity_state),
+  'scopeRows',CASE WHEN include_scope_rows THEN COALESCE((SELECT jsonb_agg(scope_data ORDER BY ord) FROM sources),'[]'::jsonb) ELSE NULL END
+ ) INTO result;
+ IF NOT for_dashboard THEN
+  cutoff:=xs_payroll_cutoff_from_version(co,mon,result->>'version');
+  IF cutoff IS NULL THEN RETURN NULL; END IF;
+  -- Full conflicts/approval already participated in the cutoff hash; only sync metadata crosses the wire.
+  cutoff:=jsonb_build_object('approved',cutoff->'approved','monthClosed',cutoff->'monthClosed','version',cutoff->'version',
+   'complete',cutoff->'complete','pending',cutoff->'pending','datasets',cutoff->'datasets',
+   'cutoffAt',cutoff->'cutoffAt','sourceVersion',cutoff->'sourceVersion');
+ END IF;
+ RETURN result||jsonb_build_object('cutoff',cutoff);
+END $$;
+
+-- Select only identity/display fields before JSON encoding. The catalog check
+-- preserves every supported alias even on schemas that omit optional columns.
+CREATE OR REPLACE FUNCTION xs_payroll_read_users(co text) RETURNS SETOF jsonb
+LANGUAGE plpgsql STABLE AS $$
+DECLARE columns text;
+BEGIN
+ SELECT string_agg(quote_ident(attname),',' ORDER BY attnum) INTO columns FROM pg_attribute
+ WHERE attrelid='"_User"'::regclass AND attnum>0 AND NOT attisdropped
+ AND attname=ANY(ARRAY['objectId','legacyUserId','legacyUserData','legacyuserdata','realName','realname','nickname','nickName','nickname2','username','mobile','phone']);
+ RETURN QUERY EXECUTE 'SELECT to_jsonb(u) FROM (SELECT '||columns||' FROM "_User" WHERE company=$1) u' USING co;
+END $$;

+ 78 - 0
scripts/sql/payroll-source-bulk.sql

@@ -0,0 +1,78 @@
+-- Recompute authoritative source rows directly; no result cache or derived tables.
+-- Bulk lookup maps eliminate one common/appointment lookup per lesson.
+CREATE OR REPLACE FUNCTION xs_payroll_sources_bulk(co text)
+RETURNS SETOF jsonb LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
+BEGIN
+ IF NOT xs_payroll_authority_ready() OR EXISTS (
+  SELECT 1 FROM "CourseAppointment" WHERE company=co AND id IS NOT NULL GROUP BY id HAVING count(*)>1
+ ) THEN
+  RETURN QUERY SELECT s FROM xs_payroll_sources(co) s;
+  RETURN;
+ END IF;
+ RETURN QUERY
+ WITH common59 AS MATERIALIZED (
+  SELECT DISTINCT ON (c."itemId") c.* FROM "CommonModel" c
+  WHERE c.company=co AND c."modelId"=59
+  ORDER BY c."itemId",c."objectId"
+ ), appointment_map AS MATERIALIZED (
+  -- Join before choosing: the earliest CommonModel can refer to a missing appointment.
+  SELECT DISTINCT ON (ac."generalId"::text) ac."generalId"::text AS join_key,a AS appointment
+  FROM "CommonModel" ac JOIN "CourseAppointment" a ON a.company=co AND a.id=ac."itemId"
+  WHERE ac.company=co AND ac."modelId"=54
+  ORDER BY ac."generalId"::text,ac."objectId"
+ ), lessons AS MATERIALIZED (
+  SELECT l AS lesson,row_number() OVER () AS ord FROM "LessonRecord" l WHERE l.company=co
+ ), canonical_info AS MATERIALIZED (
+  SELECT l.lesson,l.ord,c AS common,
+   to_jsonb(COALESCE(c."generalId",(l.lesson)."legacyGeneralId",0))#>>'{}' AS legacy_text,
+   CASE WHEN COALESCE(c."generalId",(l.lesson)."legacyGeneralId",0)>0
+    THEN 'legacy:'||COALESCE(c."generalId",(l.lesson)."legacyGeneralId")::text
+    ELSE 'lesson:'||(l.lesson)."objectId" END AS standard_key
+  FROM lessons l LEFT JOIN common59 c ON c."itemId"=(l.lesson).id
+ ), canonical_keys AS MATERIALIZED (
+  SELECT legacy_text FROM canonical_info
+ ), facts AS MATERIALIZED (
+  SELECT f AS fact,row_number() OVER () AS ord,
+   CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END AS standard_key
+  FROM "LessonRecordFact" f WHERE f.company=co AND NOT EXISTS (
+   SELECT 1 FROM canonical_keys k WHERE k.legacy_text=f."legacyGeneralId"::text AND COALESCE(f."legacyGeneralId",0)>0
+  )
+ ), ambiguous AS MATERIALIZED (
+  SELECT EXISTS (SELECT 1 FROM (
+   SELECT standard_key FROM canonical_info UNION ALL SELECT standard_key FROM facts
+  ) k GROUP BY standard_key HAVING count(*)>1) AS value
+ ), output AS (
+ SELECT 0 AS source_group,x.ord,
+ (to_jsonb(l)-'kzsj')||jsonb_build_object(
+  'lessonObjectId',l."objectId",'legacyGeneralId',COALESCE(c."generalId",l."legacyGeneralId",0),
+  'standardLessonKey',CASE WHEN COALESCE(c."generalId",l."legacyGeneralId",0)>0 THEN 'legacy:'||COALESCE(c."generalId",l."legacyGeneralId")::text ELSE 'lesson:'||l."objectId" END,
+  'source','canonical','commonStatus',COALESCE(c."status",99),
+  'lessonAt',COALESCE(l."lessonAt",xs_payroll_time(l."plpjsj"),xs_payroll_time(a."jssj"),l."sourceCreatedAt",c."createdAt"),
+  'courseCategoryKey',COALESCE(NULLIF(l."courseCategoryKey",''),a."courseCategoryKey"),
+  'courseCategoryName',COALESCE(NULLIF(l."courseCategoryName",''),a."courseCategoryName"),
+  'teacherPaySnapshot',COALESCE(l."teacherPaySnapshot",a."teacherPaySnapshot"),
+  'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(l."jffs",''),a."deliveryMode",a."jffs"),
+  'teachingRuleId',a."teachingRuleId",'studentName',COALESCE(NULLIF(l."studentName",''),c."title"),
+  'courseName',COALESCE(NULLIF(l."courseName",''),NULLIF(l."kcmc",''),c."subtitle")
+ ) AS row_data
+ FROM canonical_info x
+ CROSS JOIN LATERAL (SELECT (x.lesson).*) l
+ CROSS JOIN LATERAL (SELECT (x.common).*) c
+ LEFT JOIN appointment_map am ON am.join_key=l."yyds"::text
+ LEFT JOIN LATERAL (SELECT (am.appointment).*) a ON true
+ WHERE NOT (SELECT value FROM ambiguous)
+ UNION ALL
+ SELECT 1 AS source_group,x.ord,
+ (to_jsonb(f)-'kzsj')||jsonb_build_object('lessonObjectId',f."objectId",'source','historical-fact','commonStatus',99,'courseCategoryKey',a."courseCategoryKey",'courseCategoryName',a."courseCategoryName",'teacherPaySnapshot',a."teacherPaySnapshot",'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(f."jffs",''),a."deliveryMode",a."jffs"),'teachingRuleId',a."teachingRuleId",
+  'standardLessonKey',CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END) AS row_data
+ FROM facts x CROSS JOIN LATERAL (SELECT (x.fact).*) f
+ LEFT JOIN appointment_map am ON am.join_key=f."yyds"::text
+ LEFT JOIN LATERAL (SELECT (am.appointment).*) a ON true
+ WHERE NOT (SELECT value FROM ambiguous)
+ UNION ALL
+ -- A changed join order must never choose a different duplicate representative.
+ SELECT 0,s.ord,s.row_data FROM ambiguous guard
+ CROSS JOIN LATERAL xs_payroll_sources(co) WITH ORDINALITY AS s(row_data,ord)
+ WHERE guard.value
+ ) SELECT output.row_data FROM output ORDER BY output.source_group,output.ord;
+END $$;

+ 348 - 0
scripts/sql/payroll-source-index.sql

@@ -0,0 +1,348 @@
+-- A transactionally maintained projection index, validated against CURRENT source
+-- dependencies at every read. It never defines row existence or financial authority.
+-- Writes are best effort and never require a company-wide lock. Missing/stale rows
+-- are rebuilt in the reader's snapshot; extra rows cannot become payable sources.
+BEGIN;
+CREATE TABLE IF NOT EXISTS "PayrollSourceIndex" (
+ kind text NOT NULL,company text NOT NULL,"objectId" text NOT NULL,
+ row_data jsonb NOT NULL,row_text text NOT NULL,input_signature text,
+ generation text NOT NULL,selection jsonb NOT NULL DEFAULT '{}'::jsonb,PRIMARY KEY(kind,company,"objectId")
+);
+ALTER TABLE "PayrollSourceIndex" ADD COLUMN IF NOT EXISTS selection jsonb NOT NULL DEFAULT '{}'::jsonb;
+CREATE TABLE IF NOT EXISTS "PayrollSourceIndexMeta" (
+ singleton boolean PRIMARY KEY DEFAULT true CHECK(singleton),generation text NOT NULL,
+ signature text NOT NULL,valid boolean NOT NULL DEFAULT false
+);
+REVOKE ALL ON "PayrollSourceIndex","PayrollSourceIndexMeta" FROM PUBLIC;
+-- Only model 59 participates in canonical source selection; avoid scanning the
+-- much larger combined CommonModel digest for models 54 and 60.
+CREATE INDEX IF NOT EXISTS payroll_source_index_common59_digest
+ ON "PayrollReadDigest" (company,"objectId") INCLUDE(hash) WHERE kind='common' AND model=59;
+
+-- Conservative selectors reproduce the gateway transport rule. Unknown dates
+-- remain candidates. Identity keys retain the existing SQL COALESCE text semantics.
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_metadata(data jsonb) RETURNS jsonb LANGUAGE sql STABLE AS $$
+ SELECT jsonb_build_object('standard_key',data->>'standardLessonKey',
+  'month_candidate_key',CASE WHEN data->>'lessonAt' ~ '^\d{4}-[0-9]{2}-[0-9]{2}T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]{1,3})?(Z|[+-][0-9]{2}:[0-9]{2})$'
+    AND xs_payroll_time(data->>'lessonAt') IS NOT NULL THEN to_char(xs_payroll_time(data->>'lessonAt') AT TIME ZONE 'Asia/Shanghai','YYYY-MM') END,
+  'scope_data',jsonb_build_array(data->'coachId',data->'jsmz',data->'storeId',data->'szmdid'),
+  'coach_key',COALESCE(data->>'coachId',data->>'jsmz'),
+  'appointment_key',COALESCE(data->>'appointmentGeneralId',data->>'yyds'));
+$$;
+
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_signature() RETURNS text LANGUAGE sql STABLE AS $$
+ SELECT md5(COALESCE((SELECT string_agg(c.oid||':'||c.relfilenode||':'||a.attnum||':'||a.attname||':'||a.atttypid||':'||a.atttypmod,'|' ORDER BY c.oid,a.attnum)
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace JOIN pg_attribute a ON a.attrelid=c.oid
+ WHERE n.nspname='public' AND a.attnum>0 AND NOT a.attisdropped
+ AND c.relname IN ('CommonModel','CourseAppointment','LessonRecord','LessonRecordFact','PayrollSourceIndex','PayrollSourceIndexMeta')),'')||'|'||
+ COALESCE((SELECT string_agg(t.tgrelid||':'||t.tgname||':'||t.xmin||':'||t.ctid||':'||t.tgenabled::text||':'||pg_get_triggerdef(t.oid),'|' ORDER BY t.tgrelid,t.tgname)
+ FROM pg_trigger t WHERE t.tgrelid IN ('"CommonModel"'::regclass,'"CourseAppointment"'::regclass,'"LessonRecord"'::regclass,'"LessonRecordFact"'::regclass,'"PayrollSourceIndexMeta"'::regclass) AND t.tgname IN ('xs_payroll_source_index_row','xs_payroll_source_index_truncate','xs_payroll_source_index_meta_guard')),'')||'|'||
+ COALESCE((SELECT string_agg(pg_get_functiondef(p.oid),'|' ORDER BY p.oid) FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
+ WHERE n.nspname='public' AND p.prokind='f' AND (p.proname LIKE 'xs_payroll_source_index_%' OR p.proname IN ('xs_payroll_source_entries','xs_payroll_source_entries_scoped','xs_payroll_sources_indexed','xs_payroll_source_version_indexed','xs_payroll_time'))),''));
+$$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_ready() RETURNS boolean LANGUAGE sql STABLE AS $$
+ SELECT xs_payroll_authority_ready() AND xs_payroll_digest_ready()
+ AND COALESCE((SELECT valid AND signature=xs_payroll_source_index_signature() FROM "PayrollSourceIndexMeta" WHERE singleton),false)
+ AND (SELECT count(*) FROM pg_trigger WHERE tgrelid IN ('"CommonModel"'::regclass,'"CourseAppointment"'::regclass,'"LessonRecord"'::regclass,'"LessonRecordFact"'::regclass)
+ AND tgname IN ('xs_payroll_source_index_row','xs_payroll_source_index_truncate') AND tgenabled='A')=8;
+$$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_meta_guard() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ IF NEW.valid AND (TG_OP='INSERT' OR NEW IS DISTINCT FROM OLD) AND current_setting('xiaoshu.source_index_reseed',true) IS DISTINCT FROM pg_current_xact_id()::text THEN
+  RAISE EXCEPTION 'source index requires a full reseed and new generation';
+ END IF;
+ RETURN NEW;
+END $$;
+DROP TRIGGER IF EXISTS xs_payroll_source_index_meta_guard ON "PayrollSourceIndexMeta";
+CREATE TRIGGER xs_payroll_source_index_meta_guard BEFORE INSERT OR UPDATE ON "PayrollSourceIndexMeta" FOR EACH ROW EXECUTE FUNCTION xs_payroll_source_index_meta_guard();
+ALTER TABLE "PayrollSourceIndexMeta" ENABLE ALWAYS TRIGGER xs_payroll_source_index_meta_guard;
+
+-- Only a single target source is projected. Hashes and output are produced by the
+-- same SELECT snapshot, never by a digest row which another trigger may update later.
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_target(co text,source_kind text,source_id text)
+RETURNS TABLE(row_data jsonb,row_text text,input_signature text) LANGUAGE plpgsql VOLATILE SET jit=off AS $$
+BEGIN
+ IF source_kind='lesson' THEN
+  RETURN QUERY WITH projected AS MATERIALIZED (SELECT
+(to_jsonb(l)-'kzsj')||jsonb_build_object(
+  'lessonObjectId',l."objectId",'legacyGeneralId',COALESCE(c."generalId",l."legacyGeneralId",0),
+  'standardLessonKey',CASE WHEN COALESCE(c."generalId",l."legacyGeneralId",0)>0 THEN 'legacy:'||COALESCE(c."generalId",l."legacyGeneralId")::text ELSE 'lesson:'||l."objectId" END,
+  'source','canonical','commonStatus',COALESCE(c."status",99),
+  'lessonAt',COALESCE(l."lessonAt",xs_payroll_time(l."plpjsj"),xs_payroll_time(a."jssj"),l."sourceCreatedAt",c."createdAt"),
+  'courseCategoryKey',COALESCE(NULLIF(l."courseCategoryKey",''),a."courseCategoryKey"),
+  'courseCategoryName',COALESCE(NULLIF(l."courseCategoryName",''),a."courseCategoryName"),
+  'teacherPaySnapshot',COALESCE(l."teacherPaySnapshot",a."teacherPaySnapshot"),
+  'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(l."jffs",''),a."deliveryMode",a."jffs"),
+  'teachingRuleId',a."teachingRuleId",'studentName',COALESCE(NULLIF(l."studentName",''),c."title"),
+  'courseName',COALESCE(NULLIF(l."courseName",''),NULLIF(l."kcmc",''),c."subtitle")
+ ) AS data,
+ md5(jsonb_build_array(md5(l::text),CASE WHEN c."objectId" IS NOT NULL THEN md5(c::text) END,CASE WHEN a."objectId" IS NOT NULL THEN md5(a::text) END)::text) AS signature
+
+ FROM "LessonRecord" l
+ LEFT JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c."company"=co AND c."modelId"=59 AND c."itemId"=l."id" ORDER BY c."objectId" LIMIT 1) c ON true
+ LEFT JOIN LATERAL (SELECT a.* FROM "CourseAppointment" a JOIN "CommonModel" ac ON ac."company"=co AND ac."modelId"=54 AND ac."itemId"=a."id" WHERE a."company"=co AND ac."generalId"::text=l."yyds"::text ORDER BY ac."objectId" LIMIT 1) a ON true
+ WHERE l."company"=co AND l."objectId"=source_id) SELECT p.data,p.data::text,p.signature FROM projected p;
+ ELSIF source_kind='fact' THEN
+  RETURN QUERY WITH projected AS MATERIALIZED (SELECT
+(to_jsonb(f)-'kzsj')||jsonb_build_object('lessonObjectId',f."objectId",'source','historical-fact','commonStatus',99,'courseCategoryKey',a."courseCategoryKey",'courseCategoryName',a."courseCategoryName",'teacherPaySnapshot',a."teacherPaySnapshot",'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(f."jffs",''),a."deliveryMode",a."jffs"),'teachingRuleId',a."teachingRuleId",
+  'standardLessonKey',CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END) AS data,
+ md5(jsonb_build_array(md5(f::text),CASE WHEN a."objectId" IS NOT NULL THEN md5(a::text) END)::text) AS signature
+
+ FROM "LessonRecordFact" f LEFT JOIN LATERAL (SELECT a.* FROM "CourseAppointment" a JOIN "CommonModel" ac ON ac."company"=co AND ac."modelId"=54 AND ac."itemId"=a."id" WHERE a."company"=co AND ac."generalId"::text=f."yyds"::text ORDER BY ac."objectId" LIMIT 1) a ON true WHERE f."company"=co AND f."objectId"=source_id) SELECT p.data,p.data::text,p.signature FROM projected p;
+ END IF;
+END $$;
+
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_put(co text,source_kind text,source_id text) RETURNS text LANGUAGE plpgsql VOLATILE SET jit=off AS $$
+DECLARE projected record; current_generation text;
+BEGIN
+ IF co IS NULL OR source_id IS NULL OR NOT pg_try_advisory_xact_lock(hashtext('payroll-source-index'),hashtext(co||':'||source_kind||':'||source_id)) THEN RETURN NULL;END IF;
+ SELECT generation INTO current_generation FROM "PayrollSourceIndexMeta" WHERE singleton;
+ SELECT * INTO projected FROM xs_payroll_source_index_target(co,source_kind,source_id);
+ IF NOT FOUND THEN DELETE FROM "PayrollSourceIndex" WHERE company=co AND kind=source_kind AND "objectId"=source_id;RETURN NULL;END IF;
+ INSERT INTO "PayrollSourceIndex"(kind,company,"objectId",row_data,row_text,input_signature,generation,selection) VALUES(source_kind,co,source_id,projected.row_data,projected.row_text,projected.input_signature,current_generation,xs_payroll_source_index_metadata(projected.row_data))
+ ON CONFLICT(kind,company,"objectId") DO UPDATE SET row_data=EXCLUDED.row_data,row_text=EXCLUDED.row_text,input_signature=EXCLUDED.input_signature,generation=EXCLUDED.generation,selection=EXCLUDED.selection;
+ RETURN projected.row_data->>'legacyGeneralId';
+END $$;
+-- Canonical changes alter fact visibility, not a fact's projection. The current
+-- reader decides membership; keeping hidden fact entries is safe and avoids
+-- rewriting historical facts for every lesson/comment update.
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_lesson(co text,source_id text) RETURNS void LANGUAGE plpgsql VOLATILE SET jit=off AS $$
+BEGIN PERFORM xs_payroll_source_index_put(co,'lesson',source_id);END $$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_appointment(co text,appointment_key text) RETURNS void LANGUAGE plpgsql VOLATILE SET jit=off AS $$
+DECLARE r record;
+BEGIN
+ FOR r IN SELECT 'lesson' AS kind,"objectId" FROM "LessonRecord" WHERE company=co AND yyds::text=appointment_key
+ UNION ALL SELECT 'fact',"objectId" FROM "LessonRecordFact" WHERE company=co AND yyds::text=appointment_key ORDER BY kind,"objectId" LOOP
+  PERFORM xs_payroll_source_index_put(co,r.kind,r."objectId");
+ END LOOP;
+END $$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_row() RETURNS trigger LANGUAGE plpgsql
+ SET TimeZone='Etc/GMT-8' SET DateStyle='ISO, MDY' SET IntervalStyle='postgres' SET extra_float_digits=1 SET bytea_output='hex' AS $$
+DECLARE item jsonb; r record; changes jsonb[]:=ARRAY[]::jsonb[]; seen text[]:=ARRAY[]::text[]; changed_key text;
+BEGIN
+ -- Review/model-60 synchronization can touch hundreds of thousands of rows.
+ -- It cannot affect lesson projections: skip before catalog/meta reads or JSON.
+ IF TG_TABLE_NAME='CommonModel' THEN
+  IF TG_OP='INSERT' AND NOT COALESCE(NEW."modelId" IN (54,59),false) THEN RETURN NULL;END IF;
+  IF TG_OP='DELETE' AND NOT COALESCE(OLD."modelId" IN (54,59),false) THEN RETURN NULL;END IF;
+  IF TG_OP='UPDATE' AND NOT COALESCE(OLD."modelId" IN (54,59),false) AND NOT COALESCE(NEW."modelId" IN (54,59),false) THEN RETURN NULL;END IF;
+ END IF;
+ IF NOT EXISTS(SELECT 1 FROM "PayrollSourceIndexMeta" WHERE singleton AND valid) THEN RETURN NULL;END IF;
+ IF TG_OP<>'INSERT' THEN changes:=array_append(changes,to_jsonb(OLD));END IF;
+ IF TG_OP<>'DELETE' THEN changes:=array_append(changes,to_jsonb(NEW));END IF;
+ FOREACH item IN ARRAY changes LOOP
+  IF item->>'company' IS NULL THEN CONTINUE;END IF;
+  changed_key:=CASE WHEN TG_TABLE_NAME IN ('LessonRecord','LessonRecordFact') THEN jsonb_build_array(item->>'company',item->>'objectId')::text
+   WHEN TG_TABLE_NAME='CourseAppointment' THEN jsonb_build_array(item->>'company',item->>'id')::text
+   WHEN (item->>'modelId')::numeric=59 THEN jsonb_build_array(item->>'company','59',item->>'itemId')::text
+   ELSE jsonb_build_array(item->>'company',item->>'modelId',item->>'generalId')::text END;
+  IF changed_key=ANY(seen) THEN CONTINUE;END IF;
+  seen:=array_append(seen,changed_key);
+  IF TG_TABLE_NAME='LessonRecord' THEN PERFORM xs_payroll_source_index_lesson(item->>'company',item->>'objectId');
+  ELSIF TG_TABLE_NAME='LessonRecordFact' THEN PERFORM xs_payroll_source_index_put(item->>'company','fact',item->>'objectId');
+  ELSIF TG_TABLE_NAME='CommonModel' AND (item->>'modelId')::numeric=59 THEN
+   FOR r IN SELECT "objectId" FROM "LessonRecord" WHERE company=item->>'company' AND id=(item->>'itemId')::numeric ORDER BY "objectId" LOOP
+    PERFORM xs_payroll_source_index_lesson(item->>'company',r."objectId");
+   END LOOP;
+  ELSIF TG_TABLE_NAME='CommonModel' AND (item->>'modelId')::numeric=54 THEN
+   PERFORM xs_payroll_source_index_appointment(item->>'company',item->>'generalId');
+  ELSIF TG_TABLE_NAME='CourseAppointment' THEN
+   FOR r IN SELECT DISTINCT "generalId"::text AS key FROM "CommonModel" WHERE company=item->>'company' AND "modelId"=54 AND "itemId"=(item->>'id')::numeric ORDER BY key LOOP
+    PERFORM xs_payroll_source_index_appointment(item->>'company',r.key);
+   END LOOP;
+  END IF;
+ END LOOP;
+ RETURN NULL;
+ -- Projection upkeep must not prevent a real source write. The reader verifies
+ -- every signature and recomputes missing/stale rows in its own current snapshot.
+EXCEPTION WHEN OTHERS THEN RETURN NULL;
+END $$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_truncate() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN UPDATE "PayrollSourceIndexMeta" SET valid=false WHERE singleton;RETURN NULL;END $$;
+
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_scan(co text,reuse_index boolean,include_data boolean,mon text,coach_id text,appointment_ids text[],with_text boolean)
+RETURNS TABLE(kind text,"objectId" text,row_data jsonb,row_text text,input_signature text,ordinal bigint,standard_key text,month_candidate_key text,scope_data jsonb)
+LANGUAGE sql STABLE AS $$
+ WITH common59 AS MATERIALIZED (
+  SELECT DISTINCT ON (c."itemId") c.* FROM "CommonModel" c
+  WHERE c.company=co AND c."modelId"=59
+  ORDER BY c."itemId",c."objectId"
+ ), appointment_map AS MATERIALIZED (
+  -- Join before choosing: the earliest CommonModel can refer to a missing appointment.
+  SELECT DISTINCT ON (ac."generalId"::text) ac."generalId"::text AS join_key,a AS appointment
+  FROM "CommonModel" ac JOIN "CourseAppointment" a ON a.company=co AND a.id=ac."itemId"
+  WHERE ac.company=co AND ac."modelId"=54
+  ORDER BY ac."generalId"::text,ac."objectId"
+ ), lessons AS MATERIALIZED (
+  SELECT l AS lesson,row_number() OVER () AS ord FROM "LessonRecord" l WHERE l.company=co
+ ), canonical_info AS MATERIALIZED (
+  SELECT l.lesson,l.ord,c AS common,
+   to_jsonb(COALESCE(c."generalId",(l.lesson)."legacyGeneralId",0))#>>'{}' AS legacy_text,
+   CASE WHEN COALESCE(c."generalId",(l.lesson)."legacyGeneralId",0)>0
+    THEN 'legacy:'||COALESCE(c."generalId",(l.lesson)."legacyGeneralId")::text
+    ELSE 'lesson:'||(l.lesson)."objectId" END AS standard_key
+  FROM lessons l LEFT JOIN common59 c ON c."itemId"=(l.lesson).id
+ ), canonical_keys AS MATERIALIZED (
+  SELECT legacy_text FROM canonical_info
+ ), facts AS MATERIALIZED (
+  SELECT f AS fact,row_number() OVER () AS ord,
+   CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END AS standard_key
+  FROM "LessonRecordFact" f WHERE f.company=co AND NOT EXISTS (
+   SELECT 1 FROM canonical_keys k WHERE k.legacy_text=f."legacyGeneralId"::text AND COALESCE(f."legacyGeneralId",0)>0
+  )
+ ), ambiguous AS MATERIALIZED (
+  SELECT EXISTS (SELECT 1 FROM (
+   SELECT standard_key FROM canonical_info UNION ALL SELECT standard_key FROM facts
+  ) k GROUP BY standard_key HAVING count(*)>1) AS value
+ ), canonical_dependencies AS MATERIALIZED (
+ SELECT x.*,am.appointment,
+ md5(jsonb_build_array(COALESCE(ld.hash,md5((x.lesson)::text)),CASE WHEN (x.common)."objectId" IS NOT NULL THEN COALESCE(cd.hash,md5((x.common)::text)) END,CASE WHEN (am.appointment)."objectId" IS NOT NULL THEN COALESCE(ad.hash,md5((am.appointment)::text)) END)::text) AS signature
+ FROM canonical_info x LEFT JOIN appointment_map am ON am.join_key=(x.lesson).yyds::text
+ LEFT JOIN "PayrollReadDigest" ld ON ld.kind='lesson' AND ld.company=co AND ld."objectId"=(x.lesson)."objectId"
+ LEFT JOIN "PayrollReadDigest" cd ON cd.kind='common' AND cd.model=59 AND cd.company=co AND cd."objectId"=(x.common)."objectId"
+ LEFT JOIN "PayrollReadDigest" ad ON ad.kind='appointment' AND ad.company=co AND ad."objectId"=(am.appointment)."objectId"
+ ), fact_dependencies AS MATERIALIZED (
+ SELECT x.*,am.appointment,
+ md5(jsonb_build_array(md5((x.fact)::text),CASE WHEN (am.appointment)."objectId" IS NOT NULL THEN COALESCE(ad.hash,md5((am.appointment)::text)) END)::text) AS signature
+ FROM facts x LEFT JOIN appointment_map am ON am.join_key=(x.fact).yyds::text
+ LEFT JOIN "PayrollReadDigest" ad ON ad.kind='appointment' AND ad.company=co AND ad."objectId"=(am.appointment)."objectId"
+ ), projected AS (
+ SELECT 'lesson'::text AS kind,l."objectId",0 AS source_group,x.ord,x.signature,
+ CASE WHEN include_data AND (mon IS NULL OR m.selection->>'month_candidate_key' IS NULL OR m.selection->>'month_candidate_key'=mon) AND (coach_id IS NULL OR m.selection->>'coach_key' IS NULL OR m.selection->>'coach_key'=coach_id) AND (appointment_ids IS NULL OR m.selection->>'appointment_key' IS NULL OR m.selection->>'appointment_key'=ANY(appointment_ids)) THEN CASE WHEN i.input_signature=x.signature THEN i.row_data ELSE
+(to_jsonb(l)-'kzsj')||jsonb_build_object(
+  'lessonObjectId',l."objectId",'legacyGeneralId',COALESCE(c."generalId",l."legacyGeneralId",0),
+  'standardLessonKey',CASE WHEN COALESCE(c."generalId",l."legacyGeneralId",0)>0 THEN 'legacy:'||COALESCE(c."generalId",l."legacyGeneralId")::text ELSE 'lesson:'||l."objectId" END,
+  'source','canonical','commonStatus',COALESCE(c."status",99),
+  'lessonAt',COALESCE(l."lessonAt",xs_payroll_time(l."plpjsj"),xs_payroll_time(a."jssj"),l."sourceCreatedAt",c."createdAt"),
+  'courseCategoryKey',COALESCE(NULLIF(l."courseCategoryKey",''),a."courseCategoryKey"),
+  'courseCategoryName',COALESCE(NULLIF(l."courseCategoryName",''),a."courseCategoryName"),
+  'teacherPaySnapshot',COALESCE(l."teacherPaySnapshot",a."teacherPaySnapshot"),
+  'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(l."jffs",''),a."deliveryMode",a."jffs"),
+  'teachingRuleId',a."teachingRuleId",'studentName',COALESCE(NULLIF(l."studentName",''),c."title"),
+  'courseName',COALESCE(NULLIF(l."courseName",''),NULLIF(l."kcmc",''),c."subtitle")
+ ) END END AS data,
+ CASE WHEN with_text THEN CASE WHEN i.input_signature=x.signature THEN i.row_text||'' ELSE ((to_jsonb(l)-'kzsj')||jsonb_build_object(
+  'lessonObjectId',l."objectId",'legacyGeneralId',COALESCE(c."generalId",l."legacyGeneralId",0),
+  'standardLessonKey',CASE WHEN COALESCE(c."generalId",l."legacyGeneralId",0)>0 THEN 'legacy:'||COALESCE(c."generalId",l."legacyGeneralId")::text ELSE 'lesson:'||l."objectId" END,
+  'source','canonical','commonStatus',COALESCE(c."status",99),
+  'lessonAt',COALESCE(l."lessonAt",xs_payroll_time(l."plpjsj"),xs_payroll_time(a."jssj"),l."sourceCreatedAt",c."createdAt"),
+  'courseCategoryKey',COALESCE(NULLIF(l."courseCategoryKey",''),a."courseCategoryKey"),
+  'courseCategoryName',COALESCE(NULLIF(l."courseCategoryName",''),a."courseCategoryName"),
+  'teacherPaySnapshot',COALESCE(l."teacherPaySnapshot",a."teacherPaySnapshot"),
+  'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(l."jffs",''),a."deliveryMode",a."jffs"),
+  'teachingRuleId',a."teachingRuleId",'studentName',COALESCE(NULLIF(l."studentName",''),c."title"),
+  'courseName',COALESCE(NULLIF(l."courseName",''),NULLIF(l."kcmc",''),c."subtitle")
+ ))::text END END AS text_value,m.selection
+ FROM canonical_dependencies x CROSS JOIN LATERAL (SELECT (x.lesson).*) l
+ CROSS JOIN LATERAL (SELECT (x.common).*) c CROSS JOIN LATERAL (SELECT (x.appointment).*) a
+ LEFT JOIN "PayrollSourceIndex" i ON reuse_index AND i.kind='lesson' AND i.company=co AND i."objectId"=l."objectId" AND i.generation=(SELECT generation FROM "PayrollSourceIndexMeta" WHERE singleton)
+ CROSS JOIN LATERAL (SELECT CASE WHEN i.input_signature=x.signature THEN i.selection ELSE xs_payroll_source_index_metadata((to_jsonb(l)-'kzsj')||jsonb_build_object(
+  'lessonObjectId',l."objectId",'legacyGeneralId',COALESCE(c."generalId",l."legacyGeneralId",0),
+  'standardLessonKey',CASE WHEN COALESCE(c."generalId",l."legacyGeneralId",0)>0 THEN 'legacy:'||COALESCE(c."generalId",l."legacyGeneralId")::text ELSE 'lesson:'||l."objectId" END,
+  'source','canonical','commonStatus',COALESCE(c."status",99),
+  'lessonAt',COALESCE(l."lessonAt",xs_payroll_time(l."plpjsj"),xs_payroll_time(a."jssj"),l."sourceCreatedAt",c."createdAt"),
+  'courseCategoryKey',COALESCE(NULLIF(l."courseCategoryKey",''),a."courseCategoryKey"),
+  'courseCategoryName',COALESCE(NULLIF(l."courseCategoryName",''),a."courseCategoryName"),
+  'teacherPaySnapshot',COALESCE(l."teacherPaySnapshot",a."teacherPaySnapshot"),
+  'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(l."jffs",''),a."deliveryMode",a."jffs"),
+  'teachingRuleId',a."teachingRuleId",'studentName',COALESCE(NULLIF(l."studentName",''),c."title"),
+  'courseName',COALESCE(NULLIF(l."courseName",''),NULLIF(l."kcmc",''),c."subtitle")
+ )) END AS selection OFFSET 0) m
+ WHERE NOT (SELECT value FROM ambiguous)
+ UNION ALL
+ SELECT 'fact',f."objectId",1,x.ord,x.signature,
+ CASE WHEN include_data AND (mon IS NULL OR m.selection->>'month_candidate_key' IS NULL OR m.selection->>'month_candidate_key'=mon) AND (coach_id IS NULL OR m.selection->>'coach_key' IS NULL OR m.selection->>'coach_key'=coach_id) AND (appointment_ids IS NULL OR m.selection->>'appointment_key' IS NULL OR m.selection->>'appointment_key'=ANY(appointment_ids)) THEN CASE WHEN i.input_signature=x.signature THEN i.row_data ELSE
+(to_jsonb(f)-'kzsj')||jsonb_build_object('lessonObjectId',f."objectId",'source','historical-fact','commonStatus',99,'courseCategoryKey',a."courseCategoryKey",'courseCategoryName',a."courseCategoryName",'teacherPaySnapshot',a."teacherPaySnapshot",'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(f."jffs",''),a."deliveryMode",a."jffs"),'teachingRuleId',a."teachingRuleId",
+  'standardLessonKey',CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END) END END,
+ CASE WHEN with_text THEN CASE WHEN i.input_signature=x.signature THEN i.row_text||'' ELSE ((to_jsonb(f)-'kzsj')||jsonb_build_object('lessonObjectId',f."objectId",'source','historical-fact','commonStatus',99,'courseCategoryKey',a."courseCategoryKey",'courseCategoryName',a."courseCategoryName",'teacherPaySnapshot',a."teacherPaySnapshot",'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(f."jffs",''),a."deliveryMode",a."jffs"),'teachingRuleId',a."teachingRuleId",
+  'standardLessonKey',CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END))::text END END,m.selection
+ FROM fact_dependencies x CROSS JOIN LATERAL (SELECT (x.fact).*) f CROSS JOIN LATERAL (SELECT (x.appointment).*) a
+ LEFT JOIN "PayrollSourceIndex" i ON reuse_index AND i.kind='fact' AND i.company=co AND i."objectId"=f."objectId" AND i.generation=(SELECT generation FROM "PayrollSourceIndexMeta" WHERE singleton)
+ CROSS JOIN LATERAL (SELECT CASE WHEN i.input_signature=x.signature THEN i.selection ELSE xs_payroll_source_index_metadata((to_jsonb(f)-'kzsj')||jsonb_build_object('lessonObjectId',f."objectId",'source','historical-fact','commonStatus',99,'courseCategoryKey',a."courseCategoryKey",'courseCategoryName',a."courseCategoryName",'teacherPaySnapshot',a."teacherPaySnapshot",'durationMinutes',a."durationMinutes",'deliveryMode',COALESCE(NULLIF(f."jffs",''),a."deliveryMode",a."jffs"),'teachingRuleId',a."teachingRuleId",
+  'standardLessonKey',CASE WHEN COALESCE(f."legacyGeneralId",0)>0 THEN 'legacy:'||f."legacyGeneralId"::text ELSE 'fact:'||f."objectId" END)) END AS selection OFFSET 0) m
+ WHERE NOT (SELECT value FROM ambiguous)
+ UNION ALL
+ SELECT CASE WHEN s.row_data->>'source'='canonical' THEN 'lesson' ELSE 'fact' END,s.row_data->>'lessonObjectId',0,s.ord,NULL,CASE WHEN include_data THEN s.row_data END,CASE WHEN with_text THEN s.row_data::text END,xs_payroll_source_index_metadata(s.row_data)
+ FROM ambiguous guard CROSS JOIN LATERAL xs_payroll_sources(co) WITH ORDINALITY AS s(row_data,ord) WHERE guard.value
+ ) SELECT p.kind,p."objectId",p.data,p.text_value,p.signature,CASE WHEN include_data THEN CASE WHEN p.source_group=0 THEN p.ord ELSE (SELECT count(*) FROM lessons)+p.ord END END,p.selection->>'standard_key',p.selection->>'month_candidate_key',p.selection->'scope_data'
+ FROM projected p;
+$$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_entries(co text,reuse_index boolean,include_data boolean)
+RETURNS TABLE(kind text,"objectId" text,row_data jsonb,row_text text,input_signature text,ordinal bigint)
+LANGUAGE sql STABLE AS $$
+ SELECT e.kind,e."objectId",e.row_data,e.row_text,e.input_signature,e.ordinal
+ FROM xs_payroll_source_index_scan(co,reuse_index,include_data,NULL,NULL,NULL,true) e
+ ORDER BY CASE WHEN include_data THEN e.ordinal END;
+$$;
+-- Compatibility path keeps complete ordered rows. The three-argument function
+-- can inline at a version-only call, eliminating JSON and ordinal-sort branches.
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_entries(co text,reuse_index boolean DEFAULT true)
+RETURNS TABLE(kind text,"objectId" text,row_data jsonb,row_text text,input_signature text,ordinal bigint)
+LANGUAGE sql STABLE AS $$ SELECT * FROM xs_payroll_source_index_entries(co,reuse_index,true) $$;
+CREATE OR REPLACE FUNCTION xs_payroll_source_entries(co text)
+RETURNS TABLE(row_data jsonb,row_text text,ordinal bigint) LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
+BEGIN
+ IF NOT xs_payroll_source_index_ready() OR EXISTS(SELECT 1 FROM "CourseAppointment" WHERE company=co AND id IS NOT NULL GROUP BY id HAVING count(*)>1) THEN
+  RETURN QUERY SELECT s.data,s.data::text,s.ord FROM xs_payroll_sources(co) WITH ORDINALITY s(data,ord);RETURN;
+ END IF;
+ RETURN QUERY SELECT e.row_data,e.row_text,e.ordinal FROM xs_payroll_source_index_entries(co,true,true) e;
+END $$;
+CREATE OR REPLACE FUNCTION xs_payroll_sources_indexed(co text) RETURNS SETOF jsonb LANGUAGE sql STABLE AS $$
+ SELECT row_data FROM xs_payroll_source_entries(co) ORDER BY ordinal;
+$$;
+-- Every current source contributes narrow metadata and (optionally) exact text.
+-- Only selected candidates fetch their full JSON. Callers apply their final exact
+-- financial filters; unknown metadata and ambiguous source identities fail open
+-- for transport inclusion, while authority/schema failures use original sources.
+CREATE OR REPLACE FUNCTION xs_payroll_source_entries_scoped(co text,mon text DEFAULT NULL,coach_id text DEFAULT NULL,appointment_ids text[] DEFAULT NULL,with_text boolean DEFAULT true)
+RETURNS TABLE(row_data jsonb,row_text text,ordinal bigint,standard_key text,month_candidate_key text,scope_data jsonb)
+LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
+BEGIN
+ IF NOT xs_payroll_source_index_ready() OR EXISTS(SELECT 1 FROM "CourseAppointment" WHERE company=co AND id IS NOT NULL GROUP BY id HAVING count(*)>1) THEN
+  RETURN QUERY SELECT s.data,CASE WHEN with_text THEN s.data::text END,s.ord,s.data->>'standardLessonKey',m.selection->>'month_candidate_key',m.selection->'scope_data'
+  FROM xs_payroll_sources(co) WITH ORDINALITY s(data,ord)
+  CROSS JOIN LATERAL (SELECT xs_payroll_source_index_metadata(s.data) AS selection) m;
+  RETURN;
+ END IF;
+ RETURN QUERY SELECT e.row_data,e.row_text,e.ordinal,e.standard_key,e.month_candidate_key,e.scope_data
+ FROM xs_payroll_source_index_scan(co,true,true,mon,coach_id,appointment_ids,with_text) e;
+END $$;
+
+CREATE OR REPLACE FUNCTION xs_payroll_source_version_indexed(co text) RETURNS text LANGUAGE plpgsql STABLE SET jit=off SET work_mem='64MB' AS $$
+BEGIN
+ IF NOT xs_payroll_source_index_ready() OR EXISTS(SELECT 1 FROM "CourseAppointment" WHERE company=co AND id IS NOT NULL GROUP BY id HAVING count(*)>1) THEN RETURN xs_payroll_source_version(co);END IF;
+ RETURN (
+ SELECT md5(COALESCE((SELECT string_agg(row_text,'|' ORDER BY row_text) FROM xs_payroll_source_index_entries(co,true,false)),'')||
+ COALESCE((SELECT string_agg(to_jsonb(r)::text,'|' ORDER BY r."objectId") FROM "TeachingCompensationRule" r WHERE r."company"=co),'')||
+ COALESCE((SELECT string_agg(to_jsonb(p)::text,'|' ORDER BY p."objectId") FROM "CourseTeachingProfile" p WHERE p."company"=co),'')||
+ COALESCE((SELECT string_agg(to_jsonb(p)::text,'|' ORDER BY p."objectId") FROM "CoachPayRate" p WHERE p."company"=co),'')||
+ COALESCE((SELECT string_agg(to_jsonb(r)::text,'|' ORDER BY r."objectId") FROM "ReviewRewardIncome" r WHERE r."company"=co AND r."status"='earned'),'')));
+END $$;
+
+CREATE OR REPLACE FUNCTION xs_payroll_source_index_rebuild() RETURNS void LANGUAGE plpgsql SET jit=off SET work_mem='64MB' AS $$
+DECLARE new_generation text:=md5(clock_timestamp()::text||random()::text); co record; prior_setting text:=current_setting('xiaoshu.source_index_reseed',true);
+BEGIN
+ IF NOT xs_payroll_authority_ready() OR NOT xs_payroll_digest_ready() THEN RAISE EXCEPTION 'authoritative source and digest prerequisites required for source index rebuild';END IF;
+ LOCK TABLE "CommonModel","CourseAppointment","LessonRecord","LessonRecordFact" IN SHARE ROW EXCLUSIVE MODE;
+ PERFORM set_config('xiaoshu.source_index_reseed',pg_current_xact_id()::text,true);
+ INSERT INTO "PayrollSourceIndexMeta" VALUES(true,new_generation,'',false) ON CONFLICT(singleton) DO UPDATE SET generation=EXCLUDED.generation,signature='',valid=false;
+ DELETE FROM "PayrollSourceIndex";
+ FOR co IN SELECT DISTINCT company FROM "LessonRecord" WHERE company IS NOT NULL UNION SELECT DISTINCT company FROM "LessonRecordFact" WHERE company IS NOT NULL LOOP
+  INSERT INTO "PayrollSourceIndex"(kind,company,"objectId",row_data,row_text,input_signature,generation,selection) SELECT e.kind,co.company,e."objectId",e.row_data,e.row_text,e.input_signature,new_generation,xs_payroll_source_index_metadata(e.row_data) FROM xs_payroll_source_index_entries(co.company,false,true) e;
+ END LOOP;
+ UPDATE "PayrollSourceIndexMeta" SET signature=xs_payroll_source_index_signature(),valid=true WHERE singleton;
+ PERFORM set_config('xiaoshu.source_index_reseed',COALESCE(prior_setting,''),true);
+END $$;
+DO $$ DECLARE tab text;BEGIN
+ FOREACH tab IN ARRAY ARRAY['CommonModel','CourseAppointment','LessonRecord','LessonRecordFact'] LOOP
+  EXECUTE format('LOCK TABLE %I IN SHARE ROW EXCLUSIVE MODE',tab);
+  EXECUTE format('DROP TRIGGER IF EXISTS xs_payroll_source_index_row ON %I',tab);
+  EXECUTE format('DROP TRIGGER IF EXISTS xs_payroll_source_index_truncate ON %I',tab);
+  EXECUTE format('CREATE TRIGGER xs_payroll_source_index_row AFTER INSERT OR UPDATE OR DELETE ON %I FOR EACH ROW EXECUTE FUNCTION xs_payroll_source_index_row()',tab);
+  EXECUTE format('CREATE TRIGGER xs_payroll_source_index_truncate AFTER TRUNCATE ON %I FOR EACH STATEMENT EXECUTE FUNCTION xs_payroll_source_index_truncate()',tab);
+  EXECUTE format('ALTER TABLE %I ENABLE ALWAYS TRIGGER xs_payroll_source_index_row',tab);
+  EXECUTE format('ALTER TABLE %I ENABLE ALWAYS TRIGGER xs_payroll_source_index_truncate',tab);
+ END LOOP;
+END $$;
+SELECT xs_payroll_source_index_rebuild();
+COMMIT;

+ 50 - 0
scripts/sql/teacher-earned-months.sql

@@ -0,0 +1,50 @@
+-- Derived earned-income summaries, not PayrollBatch settlements or payments.
+-- Every reader supplies a signature of fresh inputs and the complete calculator.
+CREATE TABLE IF NOT EXISTS "TeacherEarnedMonth" (
+ company text NOT NULL,"coachId" numeric NOT NULL,month text NOT NULL,
+ signature text NOT NULL CHECK(signature ~ '^[0-9a-f]{64}$'),
+ "amountCents" bigint NOT NULL CHECK("amountCents" BETWEEN 0 AND 9007199254740991),
+ "pendingPayCount" bigint NOT NULL CHECK("pendingPayCount">=0),
+ "lineCount" bigint NOT NULL CHECK("lineCount">=0 AND "pendingPayCount"<="lineCount"),
+ "updatedAt" timestamptz NOT NULL DEFAULT clock_timestamp(),
+ PRIMARY KEY(company,"coachId",month),
+ CHECK(month='undated' OR month ~ '^\d{4}-(0[1-9]|1[0-2])$')
+);
+REVOKE ALL ON "TeacherEarnedMonth" FROM PUBLIC;
+CREATE OR REPLACE FUNCTION xs_teacher_earned_month_guard() RETURNS trigger LANGUAGE plpgsql AS $$
+BEGIN
+ IF current_setting('xiaoshu.teacher_month_write',true) IS DISTINCT FROM 'yes' THEN RAISE EXCEPTION 'derived teacher months require controlled writer'; END IF;
+ IF TG_OP='DELETE' THEN RETURN OLD; END IF;RETURN NEW;
+END $$;
+DROP TRIGGER IF EXISTS teacher_earned_month_guard ON "TeacherEarnedMonth";
+CREATE TRIGGER teacher_earned_month_guard BEFORE INSERT OR UPDATE OR DELETE ON "TeacherEarnedMonth" FOR EACH ROW EXECUTE FUNCTION xs_teacher_earned_month_guard();
+
+CREATE OR REPLACE FUNCTION xs_teacher_earned_month_read(co text,coach numeric,months jsonb)
+RETURNS jsonb LANGUAGE sql STABLE AS $$
+ SELECT COALESCE(jsonb_agg(jsonb_build_object('month',m.month,'signature',m.signature,'amountCents',m."amountCents",'pendingPayCount',m."pendingPayCount",'lineCount',m."lineCount") ORDER BY m.month),'[]'::jsonb)
+ FROM "TeacherEarnedMonth" m WHERE m.company=co AND m."coachId"=coach
+ AND (months IS NULL OR m.month IN (SELECT jsonb_array_elements_text(months)));
+$$;
+CREATE OR REPLACE FUNCTION xs_teacher_earned_month_write(co text,coach numeric,entries jsonb)
+RETURNS integer LANGUAGE plpgsql AS $$
+DECLARE item jsonb; saved integer:=0; changed integer; previous_flag text:=current_setting('xiaoshu.teacher_month_write',true);
+BEGIN
+ IF NULLIF(co,'') IS NULL OR coach IS NULL OR jsonb_typeof(entries)<>'array' THEN RAISE EXCEPTION 'invalid teacher monthly write'; END IF;
+ PERFORM set_config('xiaoshu.teacher_month_write','yes',true);
+ FOR item IN SELECT value FROM jsonb_array_elements(entries) LOOP
+  IF COALESCE(item->>'signature','') !~ '^[0-9a-f]{64}$' OR item->>'month' IS NULL
+   OR item->>'amountCents' IS NULL OR item->>'pendingPayCount' IS NULL OR item->>'lineCount' IS NULL
+   OR (item->>'amountCents')::numeric<>trunc((item->>'amountCents')::numeric)
+   OR (item->>'pendingPayCount')::numeric<>trunc((item->>'pendingPayCount')::numeric)
+   OR (item->>'lineCount')::numeric<>trunc((item->>'lineCount')::numeric) THEN RAISE EXCEPTION 'invalid teacher monthly summary'; END IF;
+  INSERT INTO "TeacherEarnedMonth"(company,"coachId",month,signature,"amountCents","pendingPayCount","lineCount")
+  VALUES(co,coach,item->>'month',item->>'signature',(item->>'amountCents')::bigint,(item->>'pendingPayCount')::bigint,(item->>'lineCount')::bigint)
+  ON CONFLICT(company,"coachId",month) DO UPDATE SET signature=EXCLUDED.signature,"amountCents"=EXCLUDED."amountCents","pendingPayCount"=EXCLUDED."pendingPayCount","lineCount"=EXCLUDED."lineCount","updatedAt"=clock_timestamp()
+  -- An older in-flight calculation cannot overwrite a newer stored generation.
+  WHERE "TeacherEarnedMonth".signature IS NOT DISTINCT FROM item->>'expectedSignature';
+  GET DIAGNOSTICS changed=ROW_COUNT;saved:=saved+changed;
+ END LOOP;
+ PERFORM set_config('xiaoshu.teacher_month_write',COALESCE(previous_flag,''),true);
+ RETURN saved;
+END $$;
+REVOKE ALL ON FUNCTION xs_teacher_earned_month_read(text,numeric,jsonb),xs_teacher_earned_month_write(text,numeric,jsonb) FROM PUBLIC;

+ 1 - 1
scripts/teacher-earnings-transform.mjs

@@ -5,5 +5,5 @@ export function teacherEarningsSupport(operationsCode, integrityCode) {
   const pick=(code,names)=>{const file=ts.createSourceFile('gateway.js',code,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);return names.map(name=>{const node=file.statements.find(n=>ts.isFunctionDeclaration(n)&&n.name?.text===name);if(!node)throw Error('缺少工资共享函数 '+name);return node.getText(file);}).join('\n');};
   const start=operationsCode.indexOf('const TEACHING_STAGE_DEFAULTS='),end=operationsCode.indexOf('async function teachingCategoryRows(',start);
   if(start<0||end<start)throw Error('缺少工资默认规则');
-  return '\nfunction teacherEarningsCalculator(){\nfunction text(v){return String(v==null?"":v).trim();}\nfunction number(v,f=0){const n=Number(v);return Number.isFinite(n)?n:f;}\n'+operationsCode.slice(start,end)+'\n'+pick(operationsCode,['classType','durationMinutes','durationHours','defaultPayRates','rateFor','teachingRuleFor','payrollLessonAt','payrollSourceProjection','dateIso','teachingRuleProjection','teachingStageFromDatabase','courseStageFromDatabase','categoryMatchesStage','isWritingCourse'])+'\n'+pick(integrityCode,['payrollDateIso','payrollBusinessDate','payrollIdentity','payrollCalculatedLine'])+'\nreturn {project:payrollSourceProjection,calculate:payrollCalculatedLine,defaultRules:defaultTeachingRules,defaultRates:defaultPayRates,rule: r=>teachingRuleProjection(r,TEACHING_CATEGORY_LABELS),category: (r)=>{const stage=courseStageFromDatabase(r.ancestor_names);return r.confirmed===true&&TEACHING_CATEGORY_LABELS.has(r.categoryKey)&&categoryMatchesStage(r.categoryKey,stage.key)&&(!isWritingCourse(r.courseName)||Boolean(stage.key&&stage.name));}};\n}\n';
+  return '\nfunction teacherEarningsCalculator(){\nfunction text(v){return String(v==null?"":v).trim();}\nfunction number(v,f=0){const n=Number(v);return Number.isFinite(n)?n:f;}\n'+operationsCode.slice(start,end)+'\n'+pick(operationsCode,['classType','durationMinutes','durationHours','defaultPayRates','rateFor','teachingRuleFor','payrollLessonAt','payrollSourceProjection','dateIso','teachingRuleProjection','teachingStageFromDatabase','courseStageFromDatabase','categoryMatchesStage','isWritingCourse'])+'\n'+pick(integrityCode,['payrollDateIso','payrollBusinessDate','payrollIdentity','payrollCalculatedLine'])+'\nreturn {businessDate:payrollBusinessDate,project:payrollSourceProjection,calculate:payrollCalculatedLine,defaultRules:defaultTeachingRules,defaultRates:defaultPayRates,rule: r=>teachingRuleProjection(r,TEACHING_CATEGORY_LABELS),category: (r)=>{const stage=courseStageFromDatabase(r.ancestor_names);return r.confirmed===true&&TEACHING_CATEGORY_LABELS.has(r.categoryKey)&&categoryMatchesStage(r.categoryKey,stage.key)&&(!isWritingCourse(r.courseName)||Boolean(stage.key&&stage.name));}};\n}\n';
 }

+ 51 - 0
scripts/tests/fixtures/mobile-read-performance-baseline.js

@@ -0,0 +1,51 @@
+// Baseline captured before bounded concurrent mobile reads, 2026-11-10.
+async function appCompanionOverview(input,current) {
+  const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,history=input.history===true,p=mobilePage(input);
+  assertLessonTenant(current);
+  if(!coach&&input.source!=='database')return appStudentCompanionOverview(input,current,uid,p);
+  const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['a."company"=$1','a."'+(coach?'pl':'szyh')+'"=$2'];
+  const date="to_char(xs_appointment_day(a.\"bxrq\",a.\"yysj\"),'YYYY-MM-DD')";
+  const add=(sql,value)=>{args.push(value);clauses.push(sql.replace('?', '$'+args.length));};
+  if(history)clauses.push('(a."dszt"::numeric>=30 OR c."status"=-2)');else clauses.push('COALESCE(c."status",99)<>-2');
+  if(coach&&number(input.studentId))add('a."szyh"=?',String(input.studentId));
+  if(input.search)add('c."title" ILIKE ?','%'+String(input.search).trim().slice(0,100)+'%');
+  if(input.month&&/^\d{4}-\d{2}$/.test(input.month))add('LEFT('+date+',7)=?',input.month);
+  const join=' FROM "CourseAppointment" a LEFT JOIN "CommonModel" c ON c."company"=a."company" AND c."modelId"=54 AND c."itemId"=a."id"';
+  const summaryWhere=clauses.join(' AND '),summaryArgs=[...args];
+  const dates=await Psql.query('SELECT '+date+' AS date,COUNT(*) FILTER(WHERE a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2)::int finished,COUNT(*) FILTER(WHERE a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2)::int planned'+join+' WHERE '+summaryWhere+' GROUP BY '+date,summaryArgs);
+  const studentOptions=coach?await Psql.query('SELECT a."szyh" AS id,MAX('+companionStudentNameSql()+') AS name,COUNT(*)::int count'+join+' LEFT JOIN "_User" u ON u."company"=a."company" AND u."legacyUserId"::text=a."szyh" WHERE a."company"=$1 AND a."pl"=$2 AND COALESCE(c."status",99)<>-2 GROUP BY a."szyh" ORDER BY name',[DEFAULT_COMPANY_ID,String(uid)]):[];
+  if(input.dateStart)add(date+'>=?',String(input.dateStart));
+  if(input.dateEnd)add(date+'<=?',String(input.dateEnd));
+  if(input.filter==='finished')clauses.push('a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2');
+  if(input.filter==='pending'){clauses.push('a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2');add(date+'<?',appOverviewToday());}
+  const where=clauses.join(' AND '),count=await Psql.one('SELECT COUNT(*)::int total'+join+' WHERE '+where,args);
+  const rows=await Psql.query('SELECT c."generalId",c."title",c."createTime",c."status" AS "commonStatus",a.*,n."nodeName" AS "courseTitle"'+join+' LEFT JOIN "Node" n ON n."company"=a."company" AND n."nodeId"::text=a."kcid" WHERE '+where+' ORDER BY a."yysj" '+(history?'DESC':'ASC')+',a."id" DESC LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
+  const courses=coach?[]:await Psql.query('SELECT c."generalId",c."title",b.*,b."objectId" AS "bindingObjectId",n."nodeName" AS "courseTitle" FROM "CourseBinding" b JOIN "CommonModel" c ON c."company"=b."company" AND c."modelId"=58 AND c."itemId"=b."id" LEFT JOIN "Node" n ON n."company"=b."company" AND n."nodeId"::text=b."kcid" WHERE b."company"=$1 AND b."yhid"=$2 AND b."enrollmentStatus"=\'active\' AND c."status"=99 AND COALESCE(n."zstatus",99)=99 ORDER BY b."id" DESC',[DEFAULT_COMPANY_ID,String(uid)]);
+  let earningsSummary, scheduleRows=rows;
+  if(coach&&history){
+    const allRows=await Psql.query('SELECT c."generalId",c."status" AS "commonStatus",a.dszt'+join+' WHERE '+where,args);
+    const paidRows=await teacherSchedulePay(allRows,uid);
+    earningsSummary={amount:paidRows.reduce((sum,row)=>sum+(Number(row.commonStatus)!==-2&&row.payStatus==='earned'?Math.round(Number(row.teacherPay)*100):0),0)/100,pendingCount:paidRows.filter(row=>Number(row.commonStatus)!==-2&&row.payStatus!=='earned').length};
+    const payById=new Map(paidRows.map(row=>[String(row.generalId),row]));
+    scheduleRows=rows.map(row=>{
+      const pay=payById.get(String(row.generalId));
+      if(!pay)return row;
+      return {...row,teacherPay:pay.teacherPay,payStatus:pay.payStatus,payReason:pay.payReason,feedbackStatus:pay.feedbackStatus};
+    });
+  }
+  const live=input.source==='database'?await databaseAppointmentVersions({studentId:coach?input.studentId:undefined},current):await appLiveVersionSnapshot(uid,['courses','appointments'],current);return {earningsSummary,schedules:scheduleRows.map(appOverviewScheduleRow),courses:courses.map(appOverviewCourseRow),page:mobilePageMeta(count.total,p),dates,studentOptions,...mobileSource(),...live};
+}
+async function appAccountOverview(input,current) {
+  const uid=await authorizeRequestedUser(current,input.uid),groupId=number(current.get('legacyGroupId')||legacyData(current).GroupID),profile={...legacyUser(current),...legacyUserAddon(current)},today=appOverviewToday(),todayCompact=today.replace(/-/g,'');
+  if(groupId===3){const [lessons,schedules,reviews]=await Promise.all([
+    Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "kclx"=\'1\')::int AS t30,COUNT(*) FILTER (WHERE "kclx"=\'2\')::int AS t60,COUNT(*) FILTER (WHERE "kclx"=\'3\')::int AS trial FROM "LessonRecord" WHERE "company"=$1 AND "jsmz"=$2',[DEFAULT_COMPANY_ID,String(uid)]),
+    Psql.one('SELECT COUNT(*) FILTER (WHERE REPLACE(LEFT(COALESCE(NULLIF("bxrq",\'\'),"yysj"),10),\'-\',\'\')=$3)::int AS today,COUNT(*) FILTER (WHERE "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30)::int AS completed FROM "CourseAppointment" WHERE NOT EXISTS (SELECT 1 FROM "CommonModel" cancelled WHERE cancelled."company"="CourseAppointment"."company" AND cancelled."modelId"=54 AND cancelled."itemId"="CourseAppointment"."id" AND cancelled."status"=-2) AND "company"=$1 AND "pl"=$2',[DEFAULT_COMPANY_ID,String(uid),todayCompact]),
+    Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "fxzt"=\'1\')::int AS completed FROM "MemoryPracticeRecord" WHERE "company"=$1 AND "plid"=$2',[DEFAULT_COMPANY_ID,uid])
+  ]);const t30=number(lessons.t30),t60=number(lessons.t60),trial=number(lessons.trial),live=await appLiveVersionSnapshot(uid,['appointments','review','account'],current);return{profile,coachPanel:{todaySchedules:number(schedules.today),completedLessons:number(schedules.completed),...await teacherIncomeSummary(DEFAULT_COMPANY_ID,uid),reviewRecords:number(reviews.total),totalLessons:number(lessons.total),totalHours:t30*.5+t60+trial,course30:t30,course60:t60,trialLessons:trial},...live};}
+  const [legacyReviewSync,legacyAppointmentSync]=await Promise.all([syncLegacyReviews(uid,current,input.refresh===true),input.source==='database'?Promise.resolve({state:'database',items:[]}):syncLegacyAppointments('szyh',uid,input.refresh===true)]),[companion,memory]=await Promise.all([
+    Psql.one('SELECT COUNT(*) FILTER (WHERE "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30)::int AS completed,COALESCE(SUM(CASE WHEN "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30 THEN CASE WHEN "dslx"=\'1\' THEN 30 WHEN "dslx" IN (\'2\',\'3\') THEN 60 ELSE 0 END ELSE 0 END),0)::int AS minutes FROM "CourseAppointment" WHERE NOT EXISTS (SELECT 1 FROM "CommonModel" cancelled WHERE cancelled."company"="CourseAppointment"."company" AND cancelled."modelId"=54 AND cancelled."itemId"="CourseAppointment"."id" AND cancelled."status"=-2) AND "company"=$1 AND "szyh"=$2',[DEFAULT_COMPANY_ID,String(uid)]),
+    Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "fxzt"=\'1\')::int AS completed,COUNT(*) FILTER (WHERE LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)=$3)::int AS today_total,COUNT(*) FILTER (WHERE LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)=$3 AND "fxzt"=\'1\')::int AS today_completed,COUNT(*) FILTER (WHERE COALESCE("fxzt",\'0\')<>\'1\' AND LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)<=$3)::int AS due_pending,COUNT(*) FILTER (WHERE COALESCE("fxzt",\'0\')<>\'1\' AND LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)>$3)::int AS scheduled,COUNT(DISTINCT LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10))::int AS scheduled_days FROM "MemoryPracticeRecord" WHERE "company"=$1 AND "yhid"=$2',[DEFAULT_COMPANY_ID,uid,today])
+  ]);const total=number(memory.total),completed=number(memory.completed),legacyCompleted=legacyAppointmentSync.state==='current'?legacyAppointmentSync.items.filter((row)=>row.cancelled!==true&&number(row.dszt)>=30):null,completedSessions=legacyCompleted?legacyCompleted.length:number(companion.completed),completedMinutes=legacyCompleted?legacyCompleted.reduce((sum,row)=>sum+(number(row.dslx)===1?30:[2,3].includes(number(row.dslx))?60:0),0):number(companion.minutes),scopes=['appointments','review','account'],live=withLegacyAppointmentVersion(await appLiveVersionSnapshot(uid,scopes,current),scopes,legacyAppointmentSync);return{profile,companionSummary:{completedSessions,completedMinutes},antiForgetSummary:{total,completed,progress:total?Math.round(completed/total*100):0,todayTotal:number(memory.today_total),todayCompleted:number(memory.today_completed),duePending:number(memory.due_pending),scheduled:number(memory.scheduled),scheduledDays:number(memory.scheduled_days)},legacyReviewSync,legacyAppointmentSync,...live};
+}
+function learnedDate(row) { const raw = String(row.dqrq || '').trim(); return /^\d{8}$/.test(raw) ? raw.slice(0,4) + '-' + raw.slice(4,6) + '-' + raw.slice(6,8) : String(row.kywsj || '').slice(0,10); }
+function learnedAt(row) { for (const value of [row.studyCommonCreateTime,row.studyCommonCreatedAt,row.studyRecordCreatedAt,row.studyRecordUpdatedAt]) { if (!value) continue; const date = value instanceof Date ? value : new Date(value); if (!Number.isNaN(date.getTime()) && date.getUTCFullYear() > 1970) return date.toISOString(); } return learnedDate(row); }

+ 46 - 0
scripts/tests/fixtures/mobile-review-performance-baseline.js

@@ -0,0 +1,46 @@
+// Original appReviewPage captured before query plan improvements, 2026-11-10.
+async function appReviewPage(input,current) {
+  const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,p=mobilePage(input),includeMetadata=input.includeMetadata!==false,today=appOverviewToday();
+  const reviewTarget=coach&&number(input.studentId)?await appReviewTarget(current,input.studentId):coach?0:uid;
+  const refreshCoachToday=coach&&!reviewTarget&&input.scope==='today'&&p.index===1&&input.refresh===true&&uid===ownLegacyId(current);
+  if(refreshCoachToday)assertLessonTenant(current);
+  const legacyReviewSync=reviewTarget?await syncLegacyReviews(reviewTarget,current,input.refresh===true):
+    refreshCoachToday?await syncLegacyCoachTodayReviews(uid,current,today):undefined;
+  const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"::text=$2'];
+  // Empty writing records are deliberate notes reviews; other empty sources remain hidden.
+  const notesSource=notesReviewSourceSql();
+  clauses.push('NOT EXISTS (SELECT 1 FROM "CommonModel" sc JOIN "DailyStudyRecord" d ON d."company"=sc."company" AND d."id"=sc."itemId" WHERE sc."company"=m."company" AND sc."modelId"=56 AND sc."generalId"::text=m."xxjlid"::text AND COALESCE(d."learned",0)=0 AND COALESCE(NULLIF(BTRIM(d."xxqs"),\'\'),\'[]\')=\'[]\' AND NOT ('+notesSource+'))');
+  const add=(sql,value)=>{args.push(value);clauses.push(sql.replace('?', '$'+args.length));};
+  const studentsPromise=coach&&includeMetadata? Psql.query('SELECT m."yhid"::text AS id,COALESCE(MAX(NULLIF(BTRIM(u."realName"),\'\')),MAX(NULLIF(BTRIM(u."nickname"),\'\')),MAX(NULLIF(BTRIM(u."legacyUserData"->>\'HoneyName\'),\'\')),MAX(NULLIF(BTRIM(u."username"),\'\')),\'学员 \'||m."yhid"::text) AS name,COUNT(*)::int AS "taskCount" FROM "MemoryPracticeRecord" m LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+clauses.join(' AND ')+' GROUP BY m."yhid"',[...args]):Promise.resolve([]);
+  if(coach&&number(input.studentId))add('m."yhid"::text=?',String(input.studentId));
+  const baseWhere=clauses.join(' AND '),baseArgs=[...args];
+  const dateSql='LEFT(COALESCE(NULLIF(m."kywrq",\'\'),m."kywsj"),10)';
+  const summaryPromise= Psql.one('SELECT COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed,COUNT(*) FILTER(WHERE COALESCE(m."fxzt",\'0\')<>\'1\' AND '+dateSql+'<=$'+(args.length+1)+')::int due,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+')::int today,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+' AND m."fxzt"=\'1\')::int \"todayCompleted\",COUNT(*) FILTER(WHERE '+dateSql+'<$'+(args.length+1)+' AND COALESCE(m."fxzt",\'0\')<>\'1\')::int \"overduePending\" FROM "MemoryPracticeRecord" m WHERE '+baseWhere,[...args,today]);
+  const datesPromise=includeMetadata? Psql.query('SELECT '+dateSql+' AS date,COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed FROM "MemoryPracticeRecord" m WHERE '+baseWhere+' GROUP BY '+dateSql,baseArgs):Promise.resolve([]);
+  if(input.scope==='today')add(dateSql+'=?',today);
+  else if(input.scope==='overdue') {add(dateSql+'<?',today);clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');}
+  else if(input.date)add(dateSql+'=?',String(input.date));
+  if(input.month)add('LEFT('+dateSql+',7)=?',String(input.month));
+  if(input.status==='completed')clauses.push('m."fxzt"=\'1\'');
+  if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
+  const where=clauses.join(' AND '),countPromise=Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
+  // Historical createdAt is the import time; the study model preserves the original learning time.
+  const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",COALESCE(n."nodeName",CASE WHEN '+notesSource+' THEN \'语法写作复习\' END) AS "kc_title",d."dqrq" AS "learned_date",dc."createTime" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",'+reviewStudentNameSql()+' AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN "CommonModel" c ON c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN "CommonModel" dc ON dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
+  const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
+  const [students,summary,dates,count,rows,income,versions]=await Promise.all([studentsPromise,summaryPromise,datesPromise,countPromise,rowsPromise,coach&&input.includeIncome!==false?Psql.one('SELECT COALESCE(SUM(r."amount"),0) AS income FROM "MemoryPracticeRecord" m JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" WHERE '+where,args).then(row=>number(row.income)):0,appLiveVersionSnapshot(versionTarget,['review','account'],current)]);
+  return {items:rows.map(row=>{const addon=row.__addon;delete row.__addon;return{...legacyAliases({...row,...addon},'CommonModel'),money:number(row.money),rewardStatus:row.rewardStatus||'pending'};}),page:mobilePageMeta(count.total,p),summary:{...summary,todayTotal:number(summary.today),todayCompleted:number(summary.todayCompleted),todayPending:number(summary.today)-number(summary.todayCompleted),overduePending:number(summary.overduePending),income,incomeScope:input.month||'all'},dates,students,legacyReviewSync,serverToday:today,...mobileSource(),...versions};
+}
+
+async function appLiveScopeVersion(scope,targetId,current){
+  const companyId=DEFAULT_COMPANY_ID,uid=String(targetId);
+  if(scope==='learning')return appLiveVersion(await Psql.one('SELECT MAX(GREATEST(COALESCE(d."updatedAt",d."createdAt"),COALESCE(c."updatedAt",c."createdAt"))) AS changed_at,COUNT(*)::int AS total FROM "DailyStudyRecord" d LEFT JOIN "CommonModel" c ON c."company"=d."company" AND c."modelId"=56 AND c."itemId"=d."id" WHERE d."company"=$1 AND d."userId"=$2',[companyId,targetId]));
+  if(scope==='courses')return appLiveVersion(await Psql.one('SELECT MAX(GREATEST(COALESCE(b."updatedAt",b."createdAt"),COALESCE(c."updatedAt",c."createdAt"),COALESCE(n."updatedAt",n."createdAt"))) AS changed_at,COUNT(*)::int AS total FROM "CourseBinding" b LEFT JOIN "CommonModel" c ON c."company"=b."company" AND c."modelId"=58 AND c."itemId"=b."id" LEFT JOIN "Node" n ON n."company"=b."company" AND CAST(n."nodeId" AS text)=CAST(b."kcid" AS text) WHERE b."company"=$1 AND CAST(b."yhid" AS text)=$2',[companyId,uid]));
+  if(scope==='appointments')return appLiveVersion(await Psql.one('SELECT MAX(GREATEST(COALESCE(a."updatedAt",a."createdAt"),COALESCE(c."updatedAt",c."createdAt"))) AS changed_at,COUNT(*)::int AS total FROM "CourseAppointment" a LEFT JOIN "CommonModel" c ON c."company"=a."company" AND c."modelId"=54 AND c."itemId"=a."id" WHERE a."company"=$1 AND (CAST(a."szyh" AS text)=$2 OR CAST(a."pl" AS text)=$2 OR CAST(a."fxpl" AS text)=$2)',[companyId,uid]));
+  if(scope==='reading'){
+    const learner=await legacyUserObject(targetId),ownerIds=learner?[learner.id,'_User$'+learner.id]:[];if(!ownerIds.length)return ':0';
+    return appLiveVersion(await Psql.one('SELECT MAX(GREATEST(COALESCE(a."updatedAt",a."createdAt"),COALESCE(l."updatedAt",l."createdAt"))) AS changed_at,COUNT(DISTINCT a."objectId")::int+COUNT(DISTINCT l."objectId")::int AS total FROM "SurveyItem" a LEFT JOIN "SurveyLog" l ON CAST(l."surveyItem" AS text)=ANY(ARRAY[CAST(a."objectId" AS text),\'SurveyItem$\'||CAST(a."objectId" AS text)]) AND CAST(l."user" AS text)=ANY($1::text[]) WHERE CAST(a."user" AS text)=ANY($1::text[]) AND a."type"=\'reading\' AND COALESCE(a."isDeleted",FALSE)=FALSE',[ownerIds]));
+  }
+  if(scope==='review')return appLiveVersion(await Psql.one('SELECT MAX(COALESCE("updatedAt","createdAt")) AS changed_at,COUNT(*)::int AS total FROM "MemoryPracticeRecord" WHERE "company"=$1 AND (CAST("yhid" AS text)=$2 OR CAST("plid" AS text)=$2)',[companyId,uid]));
+  if(scope==='account')return appLiveVersion(await Psql.one('SELECT MAX(changed_at) AS changed_at,SUM(total)::int AS total FROM (SELECT MAX(COALESCE("updatedAt","createdAt")) changed_at,COUNT(*)::int total FROM "_User" WHERE "company"=$1 AND "legacyUserId"=$2 UNION ALL SELECT MAX(COALESCE("updatedAt","createdAt")),COUNT(*)::int FROM "LessonRecord" WHERE "company"=$1 AND (CAST("xymz" AS text)=$3 OR CAST("jsmz" AS text)=$3) UNION ALL SELECT MAX(COALESCE("updatedAt","createdAt")),COUNT(*)::int FROM "CourseCreditReservation" WHERE "company"=$1 AND "studentId"=$2) versions',[companyId,targetId,uid]));
+  return ':0';
+}

Some files were not shown because too many files changed in this diff