docs(tasks): closed-beta briefs — release path, safety net, capacity, main path; beta smoke checklist
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HwMNZEzdzmrbPhuRNmZe3C
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
48a2faddfa
commit
13bde11539
@@ -332,6 +332,10 @@
|
||||
| `TASK-rectification-refresh-r3-r4-20260913.md` | `PROGRESS-rectification-refresh-r3-r4-20260913.md` | 刷新阶段 R3(MIN_BOUNDARY_DAYS 45→30)+ R4(pratyantar 与 D9/D10 上升 Narayana);首轮出题不变。即使放宽仍有 0 题例子,定向补事另线保留(BUG-664/665) | 已实现 `ab1ade59`,已部署;2026-09-13 验收通过(实现单由执行方自拟,无产品决策记录段;收益口径见研究文档) | `codex/rectification-refresh-r3-r4-20260913` |
|
||||
| `TASK-upstream-sync6-20261007.md` | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) | **上游同步第六轮(`23be1807`)**:10-05 五个计算修正一行未合——D5/D6/D8/D11 映射(乔布斯盘四张分盘星座全不同)、Vimsopaka 权重、Yogini 顺序与出生余额、Panchadhikari 宫位参照、Narayana 并列、特殊上升、出生秒数、Neecha Bhanga 两条件、首段大运子运、岁差被覆盖;逐项对照原书核实;最先做 | 已验收(门禁补修 `2df6ee72`,已部署 staging) | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) |
|
||||
| `TASK-upstream-sync7-20261014.md` | — | **上游同步第七轮(BUG-1318 起)**:婚恋评级断语改证据档位、DK「婚姻质量 X/100」改对;KP 宫头到宫头判宫;瑜伽宫主排序去重;api_server 大运年长 365.25;星历路径只核查。不动校正打分。Grok 执行(agent 循环) | 待执行 | — |
|
||||
| `TASK-beta-main-path-20261014.md` | — | **内测主路径**:BUG-1063 他人报告 worker 读档案(逐列只读授权 + test:db);对话与报告「AI 生成,仅供参考」标识与免责;BUG-1080/283 状态;状态板按事实清账;冒烟清单 `docs/testing/beta-smoke-20261014.md`。与发布单并行 | 待执行 | — |
|
||||
| `TASK-release-path-production-20261014.md` | — | **发布通路 + 生产首发**:产品 10-14 授权改生产工作流(发布 SHA = 最近门禁成功提交 + 其后纯文档);63 个迁移清单与演练脚本、首发 runbook;生产拒绝测试验证码。Grok 不 dispatch 生产工作流 | 待执行 | — |
|
||||
| `TASK-beta-safety-net-20261014.md` | — | **内测安全网**:每日备份 + 恢复演练、探活告警、本地错误汇总、日志轮转、按 requestId 查日志。**等发布通路单合入、#58 合入后开工** | 待前置 | — |
|
||||
| `TASK-beta-capacity-20261014.md` | — | **内测容量**:按用户限流、重计算短排队与对话重试、兑换码当邀请码、staging 10/20 并发压测。**等 #58 合入后开工,压测等 #57 合入** | 待前置 | — |
|
||||
| `TASK-special-lagnas-native-20261008.md` | `PROGRESS-special-lagnas-native-20261008.md` | **特殊上升点原生计算(线上缺陷)**:sync6 U6 运行时调 PyJHora,生产镜像没有 PyJHora → staging 上 Hora/Ghati/Sree/Indu/Pranapada 全 blocked;原生实现、PyJHora 只作冻结对照、快速门禁止运行时 import jhora;最先做 | 已验收(已部署 staging `795b8845`,BUG-1273) | `codex/special-lagnas-native-20261008`(未上线;部署以 `/api/health` 为准) |
|
||||
| `TASK-ephemeris-nutation-20261008.md` | `PROGRESS-ephemeris-nutation-20261008.md` | **恒星黄道少扣章动(BUG-1275)**:行星 = 含章动热带视位置 − 平岁差,全体偏 Δψ(≤17″),大运日期差 2–3 天;改标准算法、PyJHora 冻结对照、scoring-12、v5 重跑 | 已验收(已部署 staging `db0134f9`,BUG-1275) | `codex/ephemeris-nutation-20261008`(未推送,未部署) |
|
||||
| `TASK-sync6-gate-red-20261007.md` | — | **sync6 后门禁仍红**:run 3213 快速门已过,「Frontend and database tests」开跑 20 秒后日志中断、15 分钟后判失败,后续步骤 failure 0 秒;疑运行机掉线;先 Re-run,红则本机带 Docker 跑 `CI=1 npm test` 对照 `0ed45a4b` | 已完成(Re-run 通过,BUG-1271,部署 `2df6ee72`) | — |
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# TASK · 内测容量:按用户限流、重计算满时短排队、10–20 并发冒烟压测(2026-10-14)
|
||||
|
||||
- 日期:2026-10-14
|
||||
- 来源:产品 10-14 拍板「内测跑生产」;内测体检(运维一路,`scratchpad/beta-ops-20261014.md`):重计算并发 2、满了立即 429;对话与校正没有按用户限流;仓库里没有任何并发压测结果。
|
||||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/beta-capacity-20261014`,只推这个分支
|
||||
- 验收:Claude(Linux)
|
||||
- BUG 编号:开工时核对 `docs/BUG_HISTORY.md` 最大号顺延(写作时最大号 BUG-1317;#56 sync7 从 1318 起占号,#58 已预留 BUG-1328,开工时以实际最大号为准)
|
||||
- **串行**:本单 T1 改 `frontend/src/lib/request-rate-limit.ts`,#58(`TASK-node-runtime-resources-20261013`)也改这个文件 → **#58 合入 staging 后再开 T1/T2**。T4 压测基线要在 #57(`TASK-rect-scoring-perf-20261013`,校正打分提速)合入后再量,否则测的是旧的打分耗时。T0 可以先做。
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` `48a2fadd`(开工时以最新 staging 为准,进度记录写明)。
|
||||
- staging 主机 `118.26.111.127`,生产 `118.194.235.34`(2 vCPU / 4 GB,`deploy/README.md`);**T0 先核对 staging 规格是否与生产相同**,不同时压测结论要按比例说明,不得直接套到生产。
|
||||
|
||||
## 2. 事故实证(行号按符号定位)
|
||||
|
||||
| 位置 | 现状 |
|
||||
| --- | --- |
|
||||
| `scripts/api_heavy_compute_gate.py` | 进程级 `BoundedSemaphore`,`DEFAULT_CONCURRENCY = 2`,`HeavyComputeBusy` 抛出后服务器立即回 `429 ERR_COMPUTE_BUSY` + `Retry-After: 2`,**不排队**。`HEAVY_COMPUTE_PATHS` 含 `/api/consultation_workflow`(普通对话每轮取证)和全部校正打分端点:**对话与校正抢同两个位** |
|
||||
| `scripts/jyotish_api_server.py` POST 分发 | `acquire_heavy_compute_slot(path)`;只有 `consult_card_warm_only` 预热不占位 |
|
||||
| `frontend/src/mastra/consultation-workflow.ts` `httpFailureCode` | 429 → `workflow_rate_limited`,没有按 `Retry-After` 重试,这一轮对话直接失败 |
|
||||
| `frontend/src/lib/rectification-agentic/v9/engine-client.ts` | 校正已按 `busy` 读 `Retry-After` 退避(BUG-723),不在本单改动范围 |
|
||||
| `frontend/src/lib/request-rate-limit.ts` `requestRateLimits` | 只有 locationSearch / locationTimezone / dailyStarlanguage / ephemeris / synastry / sessionWrite 六个桶;**`/api/consult`、校正回合、报告生成都没有按用户限流**(报告只有每日上限 `PERSONAL_REPORT_DAILY_LIMIT`,默认 5) |
|
||||
| 压测 | 仓库只有 6 月的压测计划与 9 月单会话容量 / 首屏性能记录,没有并发结果 |
|
||||
|
||||
推算:内测 20–50 人,只要同一时刻有 3 个以上请求在跑取证或校正打分(单次 2–5 s,#57 之后目标 < 2 s),第三个人的对话就失败。
|
||||
|
||||
## 3. 根因
|
||||
|
||||
重计算闸当初是为防止 2 vCPU 被校正扫描打满而设的「宁可拒绝」策略(见文件头注释),后来普通对话的取证也进了同一个闸,但对话侧没有任何等待或重试;按用户限流只给了轻量接口。
|
||||
|
||||
## 4. 决策记录(产品 2026-10-14)
|
||||
|
||||
1. 内测跑生产(产品 10-14)。容量问题必须在内测前处理,不能只靠「人少」。
|
||||
2. 重计算并发上限**保持 2**(主机 2 vCPU,不放大);改为「满了先等一小会儿」,等不到再明确告诉用户稍后再试。
|
||||
3. 对话、校正回合、报告生成加按用户限流;阈值见 §6 建议值,**产品可改**,写进 `requestRateLimits` 常量,环境变量不新增(减少配置面)。
|
||||
4. 内测发点数用兑换码(`redemption_codes`,一码一次)当邀请码;内测期间不开支付(`EPAY_CHAT_ENABLED=false` 保持)。每码点数由产品定(§10)。
|
||||
5. 压测在 staging 做,不压生产;登录用 staging 测试邮箱 + 固定验证码(产品在会话里提供),脚本从环境变量 `STAGING_TEST_OTP` 读,**不得写进任何文件、日志或提交**。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 不改生时校正打分、`decision_policy`、闸门与题库;不改校正侧 BUG-723 的退避逻辑(服务端排队后它自然少碰 429)。
|
||||
2. `scripts/jyotish_api_server.py` 不得增长(`tests/test_api_server_growth_contract.py`):排队逻辑写在 `scripts/api_heavy_compute_gate.py` 内,主文件只允许改调用处一两行。
|
||||
3. 排队有上界:等待者数量与等待时长都有上限;等不到仍回 429 + `Retry-After`,**不得无限挂起请求线程**。健康检查与轻量路由不得进闸(文件头约定保持)。
|
||||
4. 限流放行 / 拒绝语义与 `retryAfterSeconds` 计算沿用 `consumeRequestRateLimit`;不改 #58 加的过期清理。
|
||||
5. 前端红线(AGENTS §7):tsc 0、lint 0 error、npm test 失败名单与基线逐名相同、build `/` Static、首屏 gzip ±2%;新文案对照 `frontend/docs/VOICE.md`,不写「系统繁忙」「请稍后重试」这类客服腔,参照现有「这边没接上……」口吻。
|
||||
6. 压测不得读写真实用户数据;只用测试账号;压测结束清理测试账号会话(或写明保留原因);模型调用花费写进进度记录。
|
||||
7. 不改数据库结构(本单不需要迁移);不改 workflow;不推 staging。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
| 项 | 内容 | 验收 |
|
||||
| --- | --- | --- |
|
||||
| T0 | 核对 staging 与生产主机规格(vCPU、内存、`JYOTISH_HEAVY_COMPUTE_CONCURRENCY` 实际值);在 staging 用现有代码跑一次 T4 的压测作为**改前基线**(若 #57 未合入,记清楚) | 进度记录有规格对照与改前数字 |
|
||||
| T1 | 按用户限流:`requestRateLimits` 新增 `consult`(建议 6 次 / 分钟、60 次 / 小时两档,用两个桶)、`rectificationTurn`(建议 10 次 / 分钟)、`reportGenerate`(建议 3 次 / 小时,与每日 5 份并存)。接入 `/api/consult`、校正回合路由(`frontend/src/app/api/rectification/agent/route.ts`,开案 `cases/open` 不计入)、报告生成 `frontend/src/app/api/reports/route.ts` POST。超限回 429 + `code: "rate_limited"` + `Retry-After`,**在扣点 / 预留之前**判断,不扣点 | 单元测试:各桶边界放行 / 拒绝、拒绝时不调用扣点 RPC;路由测试断言 429 体与头 |
|
||||
| T2 | 重计算闸短排队:`acquire_heavy_compute_slot` 改为带超时等待(建议最多等 8 s,等待者上限 = 4),超时或等待者满仍抛 `HeavyComputeBusy`;`Retry-After` 不变。前端 `/api/consult` 对 `workflow_rate_limited` 按 `Retry-After` 最多重试 2 次、总等待 ≤ 15 s,期间进度行显示一句「人有点多,排一下队」(文案进 VOICE);仍失败时用一句话说明并退回本轮预留点数(沿用现有失败结算路径,核实不重复退款) | Python 测试:并发 3 个请求,第 3 个在前一个释放后拿到位;等待者满时立刻 429;超时 429。前端测试:429 → 重试 → 成功;三次都 429 → 失败文案 + 退点(复用现有结算断言) |
|
||||
| T3 | 内测点数上限:核实兑换码表能否设单码点数与批量生成(后台「兑换码」页),写一页操作说明 `docs/operations/beta-invite-codes.md`:怎么批量生成邀请码、每码点数、怎么查谁用了、怎么作废。不写真实码 | 文档;若后台缺批量生成或作废能力,写进进度记录与 BLOCKED,不在本单扩功能 |
|
||||
| T4 | 冒烟压测脚本(放 `frontend/scripts/research/beta-load-smoke.mts`,不进镜像,确认不被 `frontend/src` 引用——参照 BUG-1140 / `runtime-sources-no-test-imports.test.ts`):N 个虚拟用户(10、20 两档),每人:OTP 登录 → 首轮咨询 → 一次追问;其中 3 人改为开一次校正并答 2 题。记录:每请求状态码、首字时间、整轮完成时间、429 比例;同时在服务器侧采样 API / web 容器内存与 CPU(`docker stats` 每 5 s,执行方没有 staging SSH 时写成环境缺口,只报客户端侧) | 进度记录给改前 / 改后两组数字。**通过线**(10 并发):429(最终失败,不含重试成功)≤ 2%;5xx = 0;咨询首字 P95 ≤ 15 s、整轮 P95 ≤ 90 s;API 容器内存峰值 < 1.5 GB。20 并发只记录不设门,用来定内测同时在线上限 |
|
||||
| T5 | 文档:BUG_HISTORY(每项一个号)、CHANGELOG、VOICE(排队与限流文案)、DESIGN(进度行状态)、`docs/testing/beta-capacity-20261014.md` 真机两条(快速连发触发限流的提示;排队提示出现与消失) | 齐 |
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T2 后端排队不可让;T1 不可让(防刷与成本)。T2 前端重试文案可简化为不显示「排队」而只多等。T4 20 并发档可让;10 并发档不可让(没有数就不能拉人)。T3 文档可让为「后台已有能力清单」。
|
||||
|
||||
## 8. 验收门
|
||||
|
||||
- Python:`ruff`、`py_compile`、新测试、`tests/test_api_server_growth_contract.py`、快速门全量(`failures []` 或与基线同名)。
|
||||
- 前端:tsc / lint / npm test(与基线逐名比对,看 `# cancelled` 与退出码)/ build Static / gzip。
|
||||
- 压测:脚本可在 Claude 侧(Linux)复跑 10 并发一次,数字量级一致。
|
||||
|
||||
## 9. 开工前置
|
||||
|
||||
```
|
||||
git fetch origin --prune
|
||||
git status -sb
|
||||
git worktree add -b codex/beta-capacity-20261014 .worktrees/beta-capacity-20261014 origin/staging
|
||||
# 确认 #58 已合入 staging(request-rate-limit.ts 有过期清理)再开 T1/T2;T0 可先做
|
||||
```
|
||||
|
||||
## 10. 需产品提供 / 拍板
|
||||
|
||||
1. 限流阈值:对话 6 次 / 分钟 + 60 次 / 小时、校正 10 次 / 分钟、报告 3 次 / 小时(+ 每日 5 份)——同意或改数。
|
||||
2. 排队上限:最多等 8 s、最多 4 人排队——同意或改。
|
||||
3. 每个邀请码给多少点;内测第一批多少人(压测 20 并发的结果出来后再定同时在线上限)。
|
||||
4. 压测会花模型调用费用(约 20 人 × 2 轮 × 两次),同意在 staging 跑。
|
||||
5. 若 Grok 侧拿不到 staging 主机 SSH,服务器内存 / CPU 采样由谁跑(Claude 也没有主机权限时写成环境缺口)。
|
||||
@@ -0,0 +1,100 @@
|
||||
# TASK · 内测主路径:他人报告读不到资料、AI 生成标识与免责、状态板清账、内测冒烟清单(2026-10-14)
|
||||
|
||||
- 日期:2026-10-14
|
||||
- 来源:产品 10-14 拍板「内测跑生产」「给别人出报告内测要开」;内测体检(`scratchpad/beta-compliance-20261014.md`、`beta-defects-20261014.md`)。
|
||||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/beta-main-path-20261014`,只推这个分支
|
||||
- 验收:Claude(Linux)
|
||||
- BUG 编号:开工时核对 `docs/BUG_HISTORY.md` 最大号顺延(写作时最大号 BUG-1317;#56 从 1318 起、#58 预留 1328,以实际为准)。T1 沿用 **BUG-1063**;T2 新号;T3–T5 不取新号(状态与文档)。
|
||||
- 串行:与 #56(Python 引擎)、#57(校正打分)、#58(Node 资源)、容量单 `TASK-beta-capacity-20261014`(限流 / 排队)**不改同一文件**;T2 改 `frontend/src/components/chat-composer.tsx` 与报告阅读组件,若同期有别的单改这两个文件,以先合入者为准、后者变基。
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` `48a2fadd`(开工时以最新为准,进度记录写明)。
|
||||
|
||||
## 2. 事故实证(行号按符号定位)
|
||||
|
||||
**T1 / BUG-1063 他人报告读不到资料**(状态 investigating,09-27 静态证据):
|
||||
|
||||
- `frontend/src/lib/personal-report-worker-subject.ts::loadReportWorkerProfile` → `frontend/src/lib/subject-birth.ts::loadSubjectBirth(client, …)`,用后台 admin 客户端 `from("chart_profiles").select(CHART_SUBJECT_SELECT)`。
|
||||
- admin 客户端在自托管模式下是 `set local role service_role`(`admin-client-core.ts`)。
|
||||
- `frontend/supabase/migrations/20260718100000_repair_missing_chart_profiles.sql` 第 50 行 `revoke all on table public.chart_profiles from anon, authenticated, service_role;`,之后**没有任何迁移**给 `service_role` 授回 `chart_profiles` 的任何权限(已 grep 全部迁移:只有给 `authenticated` 的 select / 列级 insert / update / delete)。
|
||||
- 本人报告不受影响:本人出生资料在 `profiles` 表;只有「人物档案里的其他人」走 `chart_profiles`。
|
||||
- 推断后果:他人报告 worker 读资料报权限错 → `subject_unavailable` → `profile_incomplete`(可重试)→ 一直排队或最终失败退款。**未在真实库复现。**
|
||||
|
||||
**T2 AI 生成标识**:只在协议文本里(`frontend/src/lib/legal-documents.ts`「本服务的回答与报告由人工智能生成……」);报告结尾有「不构成医疗、法律或投资建议」(`personal-report-generation.ts`);**对话界面(普通对话、校正对话)与报告阅读页顶部都没有显式标识,对话里也没有免责小字**。依据:《人工智能生成合成内容标识办法》2025-09-01 施行,要求对生成的文本内容在起始、末尾或中间适当位置添加显式提示(来源:国家网信办 https://www.cac.gov.cn/2025-03/14/c_1743654685899683.htm ;解读 https://www.pwccn.com/zh/tmt/method-identifying-synthetic-content-generated-ai-sep2025.pdf )。
|
||||
|
||||
**T3 / BUG-1080 新账号首次建盘只见兑换弹窗**:修复 `dec36bce` 已在 staging(已核:`git merge-base --is-ancestor dec36bce origin/staging` 成立),状态行仍写「修复在分支,未部署」。只差真机。
|
||||
|
||||
**T4 / BUG-283 今日星语回退**:状态 regressed,但修复版本写「已被 BUG-299 覆盖」,BUG-299 为 resolved(首屏只调 `/api/daily_guidance`)。实为状态未改。
|
||||
|
||||
**T5 状态板滞后**(修复已在 staging,状态行仍写「分支 / 未部署」):
|
||||
|
||||
| BUG | 状态行现写 | 事实 |
|
||||
| --- | --- | --- |
|
||||
| 906、907 | investigating(分支已修,未部署) | `82c8aeb0` 在 staging |
|
||||
| 1080 | investigating(分支,未部署) | `dec36bce` 在 staging |
|
||||
| 1128 | 字体部分 resolved(待部署)/ JS 部分 blocked | `8112f62b` 在 staging;JS 部分 blocked 保持 |
|
||||
| 1244、1245、1247~1253、1255~1259 | fixed-pending-verify(分支 `codex/…`;部署前不标) | 均已部署 staging(见各自修复版本行或 CHANGELOG);1246 已撤回,不动 |
|
||||
| 1254 | fixed-pending-verify(需门禁连续通过) | 按门禁记录核实 |
|
||||
|
||||
## 3. 根因
|
||||
|
||||
- T1:7-18 修 `chart_profiles` 权限时按「只给登录用户」收口,没考虑后台 worker;9-27 BUG-1031 把 worker 改走 `loadSubjectBirth` 后才用到这张表;mock 客户端测不出表权限。
|
||||
- T2:合规那一轮(09-30)只做了协议与同意,没有落到界面。
|
||||
- T3–T5:多会话并行合入时只改代码不回填状态行。
|
||||
|
||||
## 4. 决策记录(产品 2026-10-14)
|
||||
|
||||
1. 内测开放「给别人出报告」→ BUG-1063 必修。
|
||||
2. **T1 修法选「给 `service_role` 授 `chart_profiles` 逐列只读」**,不选 SECURITY DEFINER 只读函数。理由:① 调用方是只在服务端运行的后台 worker,`service_role` 本来就读得到本人出生资料(`profiles`),把同一组列扩到他人档案不扩大信任边界;② 定义者函数对受信的服务端调用方不提供额外保护(它照样能传任何 user_id),却多一条读路径要维护、要和 `CHART_SUBJECT_SELECT` 同步两处;③ 列级授权能精确限制到 `CHART_SUBJECT_SELECT` 的列,用一条合同测试锁住「select 列 ⊆ 授权列」。只授 SELECT,不授写。
|
||||
3. **T2 显式标识放法**:对话——在 `ChatComposer` 组件内输入框下方加一行小字(一个组件,普通对话与校正对话两处同时生效,`page.tsx` 不改);报告——阅读页标题下的元信息行加「AI 生成」,结尾免责保留;分享出去的报告页同样有。**推翻 `frontend/DESIGN.md`「No strip below the composer」的字面**:那条针对的是常驻 44 px 空条;本单是一行说明文字、不占工具位,DESIGN 同步改写这条并写明原因。文案建议:对话「回答由 AI 生成,仅供参考,不构成医疗、法律或投资建议。」;报告「本报告由 AI 生成,仅供参考。」(对照 VOICE,产品可改字)。
|
||||
4. 隐式标识(文件元数据):下载的 PDF / Word / Markdown 写入「AI 生成」元数据——列为 T2b,可让。
|
||||
5. T4:BUG-283 改为 `superseded`(被 BUG-299 取代),不改代码;首页今日星语是否正常放进冒烟清单,真机若仍失败另立新号。
|
||||
6. T5 只按事实改状态行(「已部署 staging `<sha>`,真机欠」),**不得**把 fixed-pending-verify 改成 resolved,不改根因与结论。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 迁移只做加法(`grant select (<列>) on public.chart_profiles to service_role`),对当前已部署代码向后兼容;新迁移文件名时间戳不得与现有重复;动表的轮次**必须真跑 `npm run test:db`**(AGENTS §7.6),Grok 有 Docker。
|
||||
2. 不授 `service_role` 任何写权限;不改 RLS;不动 `authenticated` 现有授权。
|
||||
3. 前端红线(AGENTS §7):tsc 0、lint 0 error、npm test 失败名单与基线逐名相同(看 `# cancelled` 与退出码)、build `/` Static、首屏 gzip ±2%;`page.tsx` 的 `Home()` 状态数不增长;改 UI 同提交更新 DESIGN;新文案进 VOICE。改既有断言写「原值 / 新值 / 原因」三栏(先 grep `frontend/tests/` 里按 `chat-composer` 源码文本断言的合同测试,AGENTS §7.8)。
|
||||
4. 不改生时校正打分、不改对话提示词、不改报告生成内容(只加阅读页与组件上的标识)。
|
||||
5. 不写真实用户资料;测试只用公开名人或虚构盘。不改 workflow、不推 staging。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
| 项 | 内容 | 验收 |
|
||||
| --- | --- | --- |
|
||||
| T0 | **先红后绿**:在 `frontend/tests/database-chart-subject.test.ts`(或新建 `database-report-worker-subject.test.ts`)加用例:用后台 admin(`service_role`)客户端对另一个人物档案调用 `loadReportWorkerProfile` / `loadSubjectBirth`,期望读到资料。改前在本地 PostgreSQL(Docker `test:db`)跑出**权限拒绝**并把错误原文写进进度记录 | 进度记录有改前红的输出;若改前竟然是绿的(说明另有授权),停下回报,不做 T1 |
|
||||
| T1 | 新迁移:`grant select (<CHART_SUBJECT_SELECT 的全部列>) on table public.chart_profiles to service_role;`。新增合同测试:解析 `CHART_SUBJECT_SELECT` 与迁移里的授权列,断言前者 ⊆ 后者(以后加列忘了授权会红)。BUG-1063 记录补根因确认、修复、验证 | T0 用例转绿;`npm run test:db` 全量与基线比对;合同测试;BUG-1063 → fixed-pending-verify(部署 + 真机给别人出一份报告后才 resolved) |
|
||||
| T2 | 对话:`ChatComposer` 内输入框下方一行说明(`--type-overline`、`--color-ink-tertiary`,移动端不换行溢出,不挡发送键)。报告阅读页(`personal-report-page.tsx` / `personal-report-document-view.tsx` 标题区)与分享页加「本报告由 AI 生成,仅供参考。」。中英文两版(英文:「AI-generated. For reference only; not medical, legal or financial advice.」/「This report is AI-generated and for reference only.」) | 组件测试:两处对话面都渲染说明;报告中英文都有;移动端快照或断言 class;DESIGN、VOICE 更新;真机清单一条 |
|
||||
| T2b | 下载文件写入「AI 生成」元数据(PDF Producer/Subject、DOCX core properties、Markdown 首行注释——按现有导出实现能力取舍) | 单元测试读回元数据;做不到的格式写进进度记录 |
|
||||
| T3 | BUG-1080 状态行改为「修复 `dec36bce` 已部署 staging,真机欠」;冒烟清单收录 | 文档 |
|
||||
| T4 | BUG-283 状态改 `superseded(由 BUG-299 取代)`;冒烟清单收录「首页今日星语」 | 文档 |
|
||||
| T5 | 按 §2 表格逐条改状态行;每条写部署证据(提交号在 staging、对应部署记录或 CHANGELOG 行);1254 按门禁记录判断 | 文档;`tests/test_repo_privacy_markers.py` 过 |
|
||||
| T6 | 冒烟清单:把 `scratchpad/beta-smoke-checklist-20261014.md` 草稿(由 Claude 随单附上)落成 `docs/testing/beta-smoke-20261014.md`,补上 T1/T2 的条目编号 | 文档 |
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T1 不可让(内测要开他人报告)。T2 对话与报告显式标识不可让(法规);T2b 可让。T3–T6 文档项不可让(产品需要一张真实的状态板),但不阻塞 T1/T2 的合入。
|
||||
|
||||
## 8. 验收门
|
||||
|
||||
- `npm run test:db`(Grok,Docker)全量,与 staging 基线比对;Claude 侧用本机 PostgreSQL 替身复跑 T0/T1 相关用例。
|
||||
- 前端 tsc / lint / npm test / build / gzip。
|
||||
- Python 不动;若碰到 `scripts/`,跑快速门全量。
|
||||
|
||||
## 9. 开工前置
|
||||
|
||||
```
|
||||
git fetch origin --prune
|
||||
git status -sb
|
||||
git worktree add -b codex/beta-main-path-20261014 .worktrees/beta-main-path-20261014 origin/staging
|
||||
grep -rn "chat-composer" frontend/tests | head # 找按源码文本断言的合同测试
|
||||
```
|
||||
|
||||
## 10. 需产品提供 / 拍板
|
||||
|
||||
1. 同意 T1 修法(`service_role` 逐列只读)——若更倾向只读函数,告诉我,改单。
|
||||
2. 标识文案:对话「回答由 AI 生成,仅供参考,不构成医疗、法律或投资建议。」、报告「本报告由 AI 生成,仅供参考。」——同意或改字;以及同意改 DESIGN「输入框下方不放东西」这条的字面。
|
||||
3. 冒烟清单由谁走:产品用 staging 测试邮箱 + 固定验证码真机走,还是先由 Claude 用接口层(无浏览器)走一遍可接口化的条目(他人报告、「你好」不扣点、限流),再由产品补真机部分。
|
||||
4. 生产首发后,冒烟清单是否在生产再走一遍(建议走前 5 条,用产品自己的账号)。
|
||||
@@ -0,0 +1,89 @@
|
||||
# TASK · 内测安全网:每日备份、探活告警、错误汇总、日志轮转与按 requestId 查日志(2026-10-14)
|
||||
|
||||
- 日期:2026-10-14
|
||||
- 来源:产品 10-14 内测体检后拍板「内测跑生产」。体检结论:生产没有定时备份、没有探活告警、没有错误上报、容器日志不轮转,出了事既不知道也查不到(BUG-1306 取证就因日志只在容器控制台而失败)。
|
||||
- 执行方:**Grok(Windows)**,分支 `codex/beta-safety-net-20261014`,只推这个分支。Grok 碰不到生产主机:主机侧的安装与演练写成脚本 + runbook,由产品执行。
|
||||
- 验收:Claude(Linux)。
|
||||
- BUG 编号:开工时核对最大号顺延(写作时 BUG-1317)。其他已推任务书在 `docs/tasks/` 里已预占到 BUG-1346,开工时取 `docs/BUG_HISTORY.md` 与 `docs/tasks/` 已引用编号两者的最大号顺延。
|
||||
- 串行:**在 `TASK-release-path-production-20261014` 合入 staging 之后开工**(两单都改 `deploy/README.md`,本单的定时工作流也要沿用那一单的发布 SHA 规则)。与 issue #58(`TASK-node-runtime-resources-20261013`,Node 内存泄漏 / Postgres 往返 / 内存表)边界:#58 可能改 `deploy/docker-compose.server.yml` 的环境变量透传;本单改同一文件的 `logging` 段——开工前看 #58 是否已合入,合入则在其之上改,未合入则在回报里写明需 Claude 合并时解冲突。不碰 #57(校正重算)任何文件。
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` 开工时最新(写作时 `48a2fadd`;需包含发布通路单的合入提交)。
|
||||
- 现有备份能力:`.gitea/workflows/create-production-recovery.yml`(只 `workflow_dispatch`)调用 `deploy/run-production-recovery.sh`:`pg_dump` 自定义格式 → AES-256-CBC/PBKDF2 加密 → 还原到一次性库校验行数 → 删一次性库 → 上传 Gitea artifact(30 天);主机上 `/opt/jyotisha-production/backups` 保留加密文件;磁盘使用 ≥70% 拒绝。`deploy/backup-staging-postgres.sh` 只给 staging 手动用。
|
||||
- 编排:`deploy/docker-compose.server.yml` 三个服务 `api`、`web`、`caddy`(PostgreSQL 另在 `deploy/docker-compose.postgres.yml`),都有 `restart: unless-stopped` 与 healthcheck,**都没有 `logging` 段**(主机 docker daemon 的默认日志配置未知)。
|
||||
- 观测:`frontend/src/lib/agent-observability.ts` 用 `console.info("[agent-observability]", JSON.stringify(event))` 写到 web 容器标准输出;Python API 的错误也只在容器标准输出。
|
||||
|
||||
## 2. 事故实证
|
||||
|
||||
| 缺口 | 证据 | 后果 |
|
||||
| --- | --- | --- |
|
||||
| 无定时备份 | 仓库内无 `schedule:` 触发的工作流、无 cron / systemd timer 文件;生产恢复点最后一次是 08-16(run 1881) | 内测开始写入真实用户数据后,任何一次误操作或磁盘故障都可能丢失最多「上次手动备份以来」的全部数据 |
|
||||
| 无探活告警 | 仓库内无 Uptime Kuma / Prometheus / 告警配置;只有容器 healthcheck(只会重启,不会通知人) | 站点挂了只能等用户来说 |
|
||||
| 无错误上报 | 无 Sentry 类集成;错误只在容器 stdout | 用户遇到报错,团队不知道,也无法统计 |
|
||||
| 日志不轮转、留存不可控 | compose 无 `logging`;`docker compose logs --tail=100` 是唯一查法(`deploy/README.md`) | 磁盘可能被日志写满;或日志在重建容器时丢失;BUG-1306 取证失败 |
|
||||
| 不能按 requestId 查 | 观测 JSON 里有 requestId,但没有按 requestId 检索的工具或流程 | 用户报「这一轮不对」时无法还原 |
|
||||
|
||||
## 3. 根因
|
||||
|
||||
上线基础设施按「单人维护、手动发布」设计,备份只作为迁移前的一次性保险;没有任何「持续运行时」的观测与告警。内测引入真实用户后这些从可选变成必需。
|
||||
|
||||
## 4. 决策记录(产品 2026-10-14 授权)
|
||||
|
||||
1. 内测跑生产,本单是内测开放前的必需项。
|
||||
2. **授权新增定时工作流**(`.gitea/workflows/` 下新增文件,属于 10-14 对 workflow 的授权范围):每日生产备份。新增文件不得改动已有四个生产工作流的语义(那是发布通路单的范围)。
|
||||
3. **红线(产品 10-14):错误上报不得把用户资料、出生资料、对话内容、邮箱、Cookie、JWT 发给任何第三方。** 因此本单不接入托管 Sentry 类服务;错误汇总在自己的主机上完成,外发的告警只含「哪个服务、什么错误码、多少次、requestId」,不含正文。
|
||||
4. 告警渠道由产品提供(见 §10);本单实现「Webhook 地址从环境 / 主机配置读取」,默认按企业微信 / 钉钉 / 飞书群机器人 Webhook 格式(三者都只要一个 URL),没配置时只写本地日志不外发。
|
||||
5. 主机侧安装全部写成脚本 + runbook,由产品执行;Grok 不 SSH、不 dispatch 生产工作流。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 备份文件始终加密;不打印、不提交加密密钥、`.env.production*`、数据库口令;告警消息与日志汇总不含任何用户内容(只含计数、错误码、路由、requestId、时间)。
|
||||
2. 不改现有 `create-production-recovery.yml` / `run-production-recovery.sh` 的加密、还原校验、锁、磁盘检查逻辑;定时备份复用它们,不另写一套 `pg_dump`。
|
||||
3. 定时备份不要求 `main == staging == 发布 SHA`(备份不该被发布状态卡住),但仍只从 `main` 运行、只用 `main` 上的脚本。
|
||||
4. 日志轮转只改 `logging` 段,不改服务的镜像、环境、端口、healthcheck。
|
||||
5. 不新增对外开放的端口;探活从外部访问公网 `https://jyotisha.chat/api/health`,不暴露 5200 / 3000。
|
||||
6. 不写真实用户资料;测试用虚构数据。
|
||||
7. Grok 不 push main、不推 staging、不 dispatch 生产工作流。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
| 项 | 内容 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| **T1 每日备份工作流** | 新增 `.gitea/workflows/backup-production-daily.yml`:`schedule` 每天北京时间 04:00 + `workflow_dispatch`;从 main 取 `deploy/run-production-recovery.sh` 执行(复用恢复点工作流的 SSH、锁、产物上传步骤,能抽成共用脚本就抽);artifact 保留天数可配,默认 14。主机侧本地加密文件保留策略:新增清理步骤只删 `backups/` 下由本工作流生成、超过 N 天(默认 7)的 `*.dump.enc` 与对应 verify 文件,按文件名前缀严格匹配,绝不删其他文件 | 合同测试断言:有 schedule、只从 main 运行、调用同一个 recovery 脚本、清理只匹配本工作流前缀、不打印密钥;清理逻辑写成独立 shell 函数并有 bats 风格或 Python 子进程测试(虚构目录:只删过期的本前缀文件,其他文件不动) |
|
||||
| **T2 恢复演练 runbook** | `docs/operations/production-restore-drill-20261014.md`:从某一天的备份 artifact 还原到一次性库(不碰 `jyotisha` 主库)→ 核对表行数 → 删除;以及真正整库还原的步骤(停写、还原、核对、恢复服务)与预计停机时间。演练结果模板 `docs/testing/production-restore-drill-20261014.md` | 步骤可照做;演练由产品在内测开放前执行一次,结果回填模板 |
|
||||
| **T3 探活与告警** | `deploy/monitor/healthcheck-alert.sh`:每分钟 GET `https://jyotisha.chat/api/health`(超时 10s),连续 3 次失败或 `status != ok` 或 `deployment.gitCommit` 与上次不同且非发布窗口时发告警;恢复时发「已恢复」;同时检查主机磁盘 ≥80% 告警。Webhook URL 从 `/opt/jyotisha-production/.monitor.env`(0600)读取。runbook:在哪台机器装(建议 Gitea runner 所在机器,**不在生产主机本身**——主机挂了它也就挂了;磁盘检查例外,需在生产主机上单独跑)、用 cron 还是 systemd timer。不在 Gitea Actions 里每分钟跑 job(runner 负担大、间隔不可靠) | Shell 测试:用本地桩服务模拟 200 / 500 / 超时 / 字段缺失;连续 3 次才告警、恢复只发一次、无 Webhook 时只写本地日志;告警正文只含允许字段 |
|
||||
| **T4 错误汇总(不外发正文)** | `deploy/monitor/error-digest.sh`:每 5 分钟读最近 5 分钟的 `web` / `api` 容器日志(`docker compose logs --since`),按「服务 + 错误类别 / 状态码 / agent 观测里的 errorCode」计数;超过阈值(默认 5 分钟内同类 ≥10 次,或任何 5xx ≥20 次)发告警,消息里附最多 5 个 requestId;每天 09:00 发一份前一天汇总。需要时在 web / api 的错误日志里补齐结构化字段(`requestId`、`route`、`errorCode`),**不加用户内容** | 用虚构日志样本测试分类与阈值;断言告警文本不含邮箱、出生日期、会话文本(用含这些内容的样本日志验证会被丢弃) |
|
||||
| **T5 日志轮转与留存** | `deploy/docker-compose.server.yml` 与 `docker-compose.postgres.yml` 每个服务加 `logging: { driver: json-file, options: { max-size: "20m", max-file: "10" } }`(约每服务 200 MB 上限);runbook 说明主机 daemon 级默认值如何确认 | 合同测试:每个服务都有 logging 段且值一致;compose `config` 能解析(本地 `docker compose -f ... config` 跑一次) |
|
||||
| **T6 按 requestId 查日志** | `deploy/find-request-log.sh <requestId> [--since 24h]`:在 web / api / caddy 日志里检索并按时间排序输出;只读。文档写进 `deploy/README.md` 运维段 | 用虚构日志样本测试;拒绝非 UUID / 非约定格式的参数(防命令注入) |
|
||||
| **T7 文档与登记** | `deploy/README.md` 新增「运维与告警」段:备份在哪、保留多久、怎么还原、告警发到哪、怎么查一轮请求;`BLOCKED.md` 登记需产品在主机上执行的安装步骤(解除后划掉) | Claude 逐条对照脚本 |
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T1、T3、T5 不可让(内测开放的最低线:有备份、挂了有人知道、磁盘不会被日志写满)。T2 runbook 不可让,演练执行可在开放前最后一天完成。T4 可让为「内测第一周补上」,但 T6 不可让(至少能查)。
|
||||
|
||||
## 8. 验收(执行方自测 + Claude 复验)
|
||||
|
||||
- Shell:`bash -n`;`shellcheck`(有则跑);上表各项测试。
|
||||
- 合同测试:放在 `frontend/tests/staging-backend-workflows.test.ts` 同类文件或新增 `tests/test_beta_safety_net.py`。
|
||||
- 前端改动(若在 web 错误日志补字段):`tsc --noEmit` 0 错;`npm run lint` 0 error;`npm test` 失败名单与基线逐名一致。
|
||||
- Python:快速门 `scripts/run_quality_gate.py --profile quick`。
|
||||
- 隐私:`tests/test_repo_privacy_markers.py`;新增测试样本只用虚构数据。
|
||||
- 文档:`docs/BUG_HISTORY.md`(无备份 / 无告警 / 日志不轮转各记一条或合并一条,状态写清「代码已交,主机安装待产品」)、`CHANGELOG.md`(运维变化,用户不可见可写一句)、`docs/tasks/PROGRESS-beta-safety-net-20261014.md`、`docs/tasks/README.md` 状态。
|
||||
|
||||
## 9. 开工前置
|
||||
|
||||
```
|
||||
git fetch origin --prune
|
||||
git status -sb
|
||||
# 确认发布通路单已合入:git log origin/staging --oneline | grep -i release-path
|
||||
git worktree add -b codex/beta-safety-net-20261014 .worktrees/beta-safety-net-20261014 origin/staging
|
||||
```
|
||||
|
||||
## 10. 需产品提供 / 拍板
|
||||
|
||||
1. **告警发到哪**:建议企业微信 / 钉钉 / 飞书任选一个群,建一个群机器人,把 Webhook 地址交给你自己保管、写进主机 `.monitor.env`(不要发到聊天里)。不选的话只写本地日志,等于没有告警。
|
||||
2. **探活装在哪台机器**:建议 Gitea runner 所在机器(与生产主机分开);磁盘检查另装在生产主机。
|
||||
3. **备份保留**:默认 Gitea artifact 14 天 + 主机本地 7 天;有更长要求(如 30 天)告诉 Claude,注意 Gitea 服务器磁盘。
|
||||
4. **恢复演练时间**:内测开放前执行一次,约 30 分钟,不影响线上(还原到一次性库)。
|
||||
5. **每日汇总发送时间**:默认 09:00。
|
||||
@@ -0,0 +1,135 @@
|
||||
# TASK · 生产发布通路改规则 + 内测首发方案(2026-10-14)
|
||||
|
||||
- 日期:2026-10-14
|
||||
- 来源:产品 10-14 内测体检后拍板:**内测跑生产**;发布规则**授权修改**(含 `.gitea/workflows/**`)。体检证据见本单 §2。
|
||||
- 执行方:**Grok(Windows)**,分支 `codex/release-path-production-20261014`,只推这个分支。Grok 碰不到生产主机、不能 dispatch 生产工作流:本单产出的是 workflow / 脚本 / 测试 / runbook,生产上的每一步由产品在 Gitea 上点按钮执行。
|
||||
- 验收:Claude(Linux)。合入 staging 由 Claude 做;**提升 main 与生产 dispatch 只由产品做**。
|
||||
- BUG 编号:开工时核对最大号顺延(写作时 BUG-1317)。注意:其他已推任务书在 `docs/tasks/` 里已预占到 BUG-1346,开工时取 `docs/BUG_HISTORY.md` 与 `docs/tasks/` 已引用编号两者的最大号顺延。
|
||||
- 串行:与 `TASK-beta-safety-net-20261014` 都可能改 `deploy/README.md`、`AGENTS.md`;**本单先做**,安全网单在本单合入后开工。与 issue #57(校正重算)、#58(Node 进程资源)不改同一文件;若 #58 改了 `deploy/docker-compose.server.yml`,本单不碰该文件。
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` `48a2fadd`(`0be4cb29` 之后只多文档提交);最近一次含门禁改动且已部署 staging 的提交以开工时 `/api/health` 为准。
|
||||
- 生产:`https://jyotisha.chat/api/health` `deployment.gitCommit = 7b620c7a`(08-16);`origin/main = 7b620c7a`;main..staging 约 1674 个提交。
|
||||
- 生产相关工作流(全部 `workflow_dispatch`,由产品手动触发):`release-quality-gate.yml`、`create-production-recovery.yml`、`migrate-production-database.yml`、`deploy-production.yml`。Gitea Actions 记录:四者最后一次运行都在 **08-16**(release 1872/1876/1880、recovery 1881、migrate 1882、deploy 1883)。
|
||||
|
||||
## 2. 事故实证(按符号定位)
|
||||
|
||||
### 2.1 纯文档 head 卡死发布
|
||||
|
||||
`deploy-production.yml` 步骤「Validate tested revision and gate run」:
|
||||
|
||||
1. 要求 `REQUESTED_SHA` 自己有 `backend-quality-gate.yml` 的 `push / staging / success` 运行。纯文档提交不在 `deploy/gated-paths.txt` 内,**不触发门禁,永远没有这条记录**。
|
||||
2. `controller_sha`(= main head)`== staging_head`,否则拒绝。
|
||||
3. 非回滚时 `REQUESTED_SHA == staging_head`,否则 `stale production revision refused`。
|
||||
4. `controller_sha != REQUESTED_SHA` 时再找 controller 的门禁运行(纯文档 controller 同样找不到)。
|
||||
5. 要求 `REQUESTED_SHA` 有 `release-quality-gate.yml` 的 dispatch 成功运行;该工作流 checkout 的是 `gitea.sha`(被 dispatch 的分支 head),所以 head_sha 只能等于分支 head。
|
||||
6. 部署前后 `require_current_release_heads`:`current_main == DEPLOY_SHA && current_staging == DEPLOY_SHA`。
|
||||
|
||||
`create-production-recovery.yml`(`GITEA_SHA == DEPLOY_SHA`,`main_head == staging_head == DEPLOY_SHA`,release 运行 head_sha == DEPLOY_SHA)与 `migrate-production-database.yml`(同样三条 + push 门禁运行)有相同约束。
|
||||
|
||||
结果:staging 只要在最后一次代码提交之后再推过任何文档(agent 循环每天都会推),四个生产工作流全部无法通过;唯一出路是冻结 staging。产品选择改规则。
|
||||
|
||||
### 2.2 待上线迁移:63 个,全部是新增文件
|
||||
|
||||
`git diff --name-status origin/main origin/staging -- frontend/supabase/migrations frontend/db/migrations`:63 个 `A`,`frontend/db/migrations` 0 个。按顶层语句(去掉函数体后)分级:
|
||||
|
||||
| 级别 | 数量 | 迁移 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| **A 破坏性 / 改写用户数据** | 2 | `20260925020000_chart_subject_typed_columns.sql`:先 backfill 到类型化列,再顶层 `delete from public.chart_profiles where role = 'self'`(本人资料改由 `profiles` 承载)。`20260921010000_retire_chat_session_archive.sql`:`update public.chat_sessions set archived_at = null where archived_at is not null`(所有已归档会话回到列表) | 不可逆;必须先在恢复点副本上演练 |
|
||||
| **B 改写数据(范围有限)** | 5 | `20260816010000_accept_exact_family_birth_times`(`profiles` 家人精确时间直接 accepted)、`20260822010000_declared_birth_window_clocks`(按时段回填申报窗口)、`20260823010000_rectification_occupation_dateless`(职业/外貌/胎记证据置 confirmed)、`20260906030000_rectification_block_scan_stage`(minute 阶段未采用 case 改 block_scan)、`20260925030000_chart_subject_trust_and_limit`(**他人档案的 accepted / confirmed 时间全部退回 reported、清空 active_birth_***) | 演练时逐条记行数;`20260925030000` 会让他人档案失去已采用时间,需产品知情 |
|
||||
| **C 权限收紧** | 约 45 | 绝大多数文件含 `revoke`(函数 / 表 EXECUTE、SELECT 收回) | 上线后 web / worker 角色可能失权(同类问题见 BUG-1063:07-18 迁移收回了 worker 对人物资料表的权限);演练后必须跑主路径冒烟 |
|
||||
| **D 纯新增** | 其余 | 加表 / 列 / 函数 / 索引 | 低风险 |
|
||||
|
||||
注:A+B 合计 7 条是按「顶层 `delete` / `update`」机械筛出;函数体内的 `delete`(注销、清理)不计。执行方需复核每条分级并补全 C/D 清单。
|
||||
|
||||
另:重复时间戳仍有 3 组(`20260806010000`、`20260816010000`、`20260920010000`),`frontend/db/migrations` 另有 `20260807010000` 一组。迁移器按文件名记账,功能无害,本单只记录不改名(改名会让已迁移的 staging 重复执行)。
|
||||
|
||||
### 2.3 生产可以误开测试验证码
|
||||
|
||||
`frontend/src/modules/identity/config.ts::readTestOtp`:只要设置了 `IDENTITY_TEST_OTP`(6 位数字)就接受固定验证码,不区分环境。`deploy/validate-production-env.sh` 不检查这个变量。生产登录页当前负载 `testOtp: null`(只读 GET 核对),但没有任何防误配机制;内测跑生产后,误配一次即任何人可用固定码登录任意邮箱。
|
||||
|
||||
## 3. 根因
|
||||
|
||||
- 生产工作流在 08-16 前设计,当时没有「纯文档提交不触发门禁」(`deploy/gated-paths.txt` 与 `deploy/is-docs-only-range.sh` 是之后才加的),四个工作流都假设「每个 staging head 都有门禁运行」。staging 侧已用 `is-docs-only-range.sh` 解决(AGENTS §2.4),生产侧没跟上。
|
||||
- 生产停在 08-16 两个月,63 个迁移在 staging 逐个上线、逐个验证,但从未作为一批在生产数据上演练过。
|
||||
- 测试验证码只在 staging 配置文档里出现,生产校验脚本是白名单思路,没把它列为禁止项。
|
||||
|
||||
## 4. 决策记录(产品 2026-10-14 授权)
|
||||
|
||||
1. **授权修改 `.gitea/workflows/` 下的 `deploy-production.yml`、`create-production-recovery.yml`、`migrate-production-database.yml`、`release-quality-gate.yml`**(只为本单的发布规则;不改 staging 工作流的触发与部署逻辑)。这推翻 AGENTS.md §2.7「不改 `.gitea/workflows/**`」在本单范围内的限制,§2.7 其余(不自行提升 main、不动 DNS)不变。
|
||||
2. **新发布规则**(取代「请求 SHA == staging head」):
|
||||
- 记 `H` = 当前 staging head,`main` 必须快进到 `H`(继续满足 AGENTS §2.5「main 与 staging 同一 SHA」,不得 merge)。
|
||||
- 发布 SHA `R` = **最近一次 `backend-quality-gate` push 成功的 staging 提交**,并且 `deploy/is-docs-only-range.sh R H` 退出 0(R 是 H 的祖先,R..H 之间只改了门禁清单之外的路径)。
|
||||
- 由于 R..H 不改门禁路径(含 `deploy/**`、`.gitea/**`、`frontend/**`、`scripts/**`),R 与 H 的部署脚本、镜像输入完全相同;controller 一律取 R 的门禁产物。
|
||||
- `release-quality-gate` 仍从 main dispatch(跑在 H 上),记录的 head_sha = H;生产三个工作流接受「release 运行的 head_sha ∈ {R, 当前 main},且该 SHA 与 R 之间纯文档」。
|
||||
- staging 公网 `/api/health` 的 `deployment.gitCommit` 必须等于 R(与 AGENTS §2.4 一致)。
|
||||
- `allow_rollback=true` 的回滚语义不变(回滚目标仍须在 main 历史里、有门禁运行)。
|
||||
3. 同步改 AGENTS.md §2.4–§2.5 与 `deploy/README.md` 的生产发布段落,文字与新规则一致。
|
||||
4. 生产首发的执行顺序固定为:**release 门 → 恢复点(含还原验证)→ 迁移演练报告确认 → 生产迁移 → 部署 → 核对**。每一步由产品在 Gitea 点按钮;Grok 不 dispatch 任何生产工作流。
|
||||
5. 生产运行时拒绝测试验证码(双保险:env 校验脚本 + 运行时)。
|
||||
6. 不在本单做:备份定时化、监控告警、日志轮转(见 `TASK-beta-safety-net-20261014`);按用户限流与并发压测(另单);备案 / 主体 / 律师(产品后续自理)。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 不得放宽任何安全检查:仍要求 R 有 push 门禁成功运行、有 release 门成功运行、有 ≤24h 且 `restore_verified=true` 的恢复点(迁移)、`main == staging`、生产 `previous_sha` 到 R 的单向前进校验、回滚显式确认。新规则只把「等于 staging head」换成「是 staging head 且之后纯文档」,`is-docs-only-range.sh` 返回 1 或 2 一律拒绝。
|
||||
2. `is-docs-only-range.sh` 不改语义;若需在工作流里调用,用已 checkout 的 R 或 H 版本,API 模式用现有 `GITEA_TOKEN`,不新增密钥。
|
||||
3. 不改 staging 部署工作流、不改 `deploy/gated-paths.txt`(若发现必须改,停下回报)。
|
||||
4. 不在任何日志 / 回报里打印密钥、数据库口令、`.env.production*` 内容、恢复点加密密钥。
|
||||
5. 迁移文件**不改**(已在 staging 执行过,改内容会造成两库不一致);需要补救就新增迁移,并在回报里说明。
|
||||
6. Grok 不 dispatch 任何生产工作流、不 push main、不推 staging。
|
||||
7. 不写真实用户资料。演练报告只写行数和表名,不写任何行内容。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
| 项 | 内容 | 验收标准 |
|
||||
| --- | --- | --- |
|
||||
| **T1 发布 SHA 解析脚本** | 新增 `deploy/resolve-release-revision.sh`(名字可调):输入 `REQUESTED_SHA`、读 main / staging head(Gitea API),校验 `main == staging == H`、`REQUESTED_SHA` 有 push 门禁成功运行、`is-docs-only-range.sh REQUESTED_SHA H` 退出 0;输出 `release_sha`、`gate_run_id`、`head_sha` 供后续步骤使用。四个生产工作流共用这一个脚本,不各写一份 jq。 | Python 测试(mock Gitea API 响应:本地 HTTP 桩或把 curl 换成可注入的读文件函数)覆盖:R==H 通过;R<H 且纯文档通过;R<H 但中间改了 `frontend/**` 拒绝;R 不是 H 祖先拒绝;R 无门禁运行拒绝;main≠staging 拒绝;API 失败(exit 2)拒绝 |
|
||||
| **T2 deploy-production** | 「Validate tested revision」改用 T1;controller 一律取 R 的门禁产物;release 门检查接受 head_sha ∈ {R, H}(且纯文档);`require_current_release_heads` 改为 `main == staging` 且 `is-docs-only-range R current_head` 退出 0;回滚分支逻辑不变 | 扩展 `frontend/tests/staging-backend-workflows.test.ts`(或新增同类测试)对 workflow 文本做合同断言:仍检查 push 门禁、release 门、main==staging、回滚确认;不再出现「`REQUESTED_SHA != staging_head` 即拒绝」的非回滚条件;调用 T1 脚本 |
|
||||
| **T3 create-production-recovery / migrate-production-database / release-quality-gate** | recovery 与 migrate 的 `GITEA_SHA == DEPLOY_SHA`、`main == staging == DEPLOY_SHA` 改为 T1 语义(dispatch 自 main,`DEPLOY_SHA` = R);release 门不改 checkout,但在 job summary / 输出里写明「H 相对最近门禁 R 是否纯文档」 | 同 T2 的合同测试;三个工作流对「R 落后 H 且纯文档」给出同一结论 |
|
||||
| **T4 文档** | AGENTS.md §2.4–§2.5、§2.7(注明 10-14 授权范围);`deploy/README.md` 生产发布段落;新增 `docs/operations/production-release-runbook-20261014.md`:产品逐步点哪个按钮、填什么、看到什么算成功、失败怎么办 | runbook 每一步可照做;与新 workflow 文本一致(Claude 逐条对照) |
|
||||
| **T5 迁移演练脚本与报告模板** | 新增 `deploy/rehearse-production-migrations.sh`(只在主机上由产品执行):从最新恢复点解密 → 还原到一次性数据库 `rehearse_<时间>` → 记录迁移前 7 条 A/B 迁移涉及表的行数 → 按迁移器顺序执行 63 个迁移 → 记录迁移后行数与每个迁移的 `RAISE NOTICE` → 用 web 运行角色做只读冒烟(能读本人 profile、能读会话列表、worker 角色能读人物资料表——对应 BUG-1063)→ 删除一次性库 → 输出不含任何行内容的报告。复用 `run-production-recovery.sh` 的加密 / 还原 / 锁 / 磁盘检查写法,不另造。另写 `docs/testing/production-migration-rehearsal-20261014.md` 报告模板与通过判据 | 在**本地 PostgreSQL**(Docker 或 Claude 机器的替身)上用 staging 迁移前的 schema 快照跑通脚本(演示数据只能是虚构数据);判据写清:A 类两条的行数变化与预期一致、C 类冒烟全过、无迁移报错 |
|
||||
| **T6 迁移清单** | `docs/operations/production-migrations-20261014.md`:63 个迁移逐条:文件名、级别(A/B/C/D)、一句话作用、对生产用户的影响、演练要看的指标。§2.2 是初筛,执行方逐条复核 | 63 条齐全;A/B 每条有「产品需知」一句 |
|
||||
| **T7 禁止生产测试验证码** | `deploy/validate-production-env.sh`:出现 `IDENTITY_TEST_OTP=`(任何值,含空)即失败。`readTestOtp` 或其调用处:当 `AUTH_USER_ORIGIN` 为生产域名(`https://jyotisha.chat`)时,设置了测试码直接抛错拒绝启动(fail closed);staging 行为不变 | Shell 测试:生产 env 含该变量失败、不含通过;TS 测试:生产 origin + 测试码抛错、staging origin + 测试码返回码、未设置返回 null |
|
||||
| **T8 首发后核对清单** | `docs/testing/production-first-release-20261014.md`:`/api/health` 200 且 `deployment.gitCommit == R`、`apiGitCommit == R`、`swisseph_available == true`(内部 health)、`database.requiredMigrationsPresent == true` 且 `latestMigration` 等于 staging 同字段;`/login` 200 且负载 `testOtp` 为 null;未登录 `/api/account` 401;用受控邮箱走一遍登录 → 建盘 → 一次对话 → 打开报告页;后台 `/admin` 只有管理员能进 | 清单可照做,每条有预期值 |
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T1、T2、T3、T7 不可让(没有它们发不了版或有安全缺口)。T4 不可让(文档与规则不一致会让下一个人照旧文档操作)。T5 可先交脚本 + 本地验证,主机演练由产品执行后回填。T6、T8 可与 T5 一起交。
|
||||
|
||||
## 8. 验收(执行方自测 + Claude 复验)
|
||||
|
||||
- 前端:`tsc --noEmit` 0 错;`npm run lint` 0 error;`npm test` 失败名单与开工基线逐名一致(Windows 已知失败按 `tools/agent-loop/windows-known-failures.txt`)。
|
||||
- Python:新增测试 + 快速门 `scripts/run_quality_gate.py --profile quick`。
|
||||
- Shell:`bash -n` 全部新脚本;`shellcheck`(有则跑)。
|
||||
- 工作流:本地无法运行 Gitea Actions——合同测试断言 workflow 文本;Claude 合入 staging 后,`backend-quality-gate` 会跑在含新 workflow 的提交上(`.gitea/**` 是门禁路径)。生产工作流的实跑由产品首发时完成,结果回填 T8 清单。
|
||||
- 文档:`docs/BUG_HISTORY.md`(发布卡死、测试验证码各一条)、`CHANGELOG.md`(一句:生产可以在 staging 之后有文档提交时照常发布)、`docs/tasks/PROGRESS-release-path-production-20261014.md`、`docs/tasks/README.md` 状态。
|
||||
|
||||
## 9. 开工前置
|
||||
|
||||
```
|
||||
git fetch origin --prune
|
||||
git status -sb
|
||||
git worktree add -b codex/release-path-production-20261014 .worktrees/release-path-production-20261014 origin/staging
|
||||
```
|
||||
|
||||
开工时记录 `origin/staging` SHA 与 staging `/api/health` 的 `deployment.gitCommit`,写进进度记录。
|
||||
|
||||
## 9a. 生产首发步骤(写进 T4 runbook,由产品执行,本单合入 staging 并部署 staging 之后)
|
||||
|
||||
1. 确认 staging `/api/health` 的 gitCommit = R,且 `deploy/is-docs-only-range.sh R <staging head>` 退出 0。
|
||||
2. 产品把 main 快进到 staging head:`git push origin <staging head>:main`(或 Gitea 界面快进),不得 merge。
|
||||
3. Gitea → Release Quality Gate,Use workflow from: `main`,等成功。
|
||||
4. Gitea → Create Production Recovery Point,`deploy_sha = R`,记下 `recovery_reference`、`recovery_created_at`。
|
||||
5. 主机上按 T5 脚本演练迁移(或由 Claude 远程指导),报告填进 `docs/testing/production-migration-rehearsal-20261014.md`;A 类两条行数、C 类冒烟全部符合判据才继续。
|
||||
6. Gitea → Migrate Production Database,填 R、恢复点引用、时间、`restore_verified=true`(24 小时内)。
|
||||
7. Gitea → Deploy production,`deploy_sha = R`,`allow_rollback=false`,`verification_mode=public`。
|
||||
8. 按 T8 清单核对。失败:应用层用 Deploy production `allow_rollback=true` 回到 `7b620c7a`(注意:迁移已执行后旧代码可能读不了新 schema——回滚以「恢复点还原 + 回滚应用」为准,runbook 里写清由谁、怎么还原)。
|
||||
|
||||
## 10. 需产品提供 / 拍板
|
||||
|
||||
1. **首发时间窗**:建议选晚上低峰,整个流程约 1–2 小时;首发前一天冻结 agent 循环向 staging 推代码(文档可以推)。
|
||||
2. **`20260925030000` 他人档案已采用时间退回 reported**:内测前接受,还是另立补救迁移(例如只退回无校正记录的)——演练报告出来后拍板。
|
||||
3. **`20260921010000` 已归档会话全部回到列表**:确认这是想要的(该迁移对应「取消归档功能」的产品决定)。
|
||||
4. **迁移演练在哪执行**:主机上由产品执行脚本,或给 Claude 一次受控的主机只读 + 一次性库操作权限(默认前者)。
|
||||
5. **回滚口径**:首发失败时是否接受「恢复点整库还原」(会丢失首发后写入的数据);内测开放前失败则无影响。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 内测前冒烟清单(草稿,2026-10-14)
|
||||
|
||||
> 先在 staging 走,生产首发后用产品自己的账号再走前 5 条。登录:staging 任意测试邮箱 + 固定验证码(产品掌握,不写进本文件)。每条填「结果(通过 / 不通过 / 没走)」「日期」「备注(截图路径)」。不通过的写现象原话,Claude 立单。
|
||||
> 账号准备:后台「兑换码」页生成测试码,给测试账号兑换点数(新账号默认 0 点)。
|
||||
|
||||
| # | 功能 | 前置 | 步骤 | 预期 | 对应 BUG / 原清单 | 结果 | 日期 | 备注 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 新人第一次进站 | 一个从没用过的邮箱;手机 Safari | 打开登录页 → 勾选同意 → 输入邮箱 → 发验证码 → 输入固定码 → 填出生资料(城市、日期、时间) | 不要求设密码;建盘后先看到群星汇聚过场,**不先弹兑换窗**;过场后进入首页 | BUG-1080、1152;`staging-otp-skip-password-20261001.md`、`birth-sky-cover-checklist.md` | | | |
|
||||
| 2 | 首页 | 第 1 条的账号 | 停留首页、刷新一次 | 不卡在「正在载入账户」;今日星语有当天内容(不是「没写出来」);会话列表正常 | BUG-936、283 / 299;`home-first-load-20260930.md`、`session-list-rebuild-20260921.md` | | | |
|
||||
| 3 | 寒暄不扣点 | 记下当前点数 | 新对话发「你好」 | 一句简短回应,不排盘、不出长解读;点数不变 | BUG-976、977;`consult-smalltalk-fastpath-20260920.md` | | | |
|
||||
| 4 | 普通对话 + AI 标识 | 有点数 | 问一个事业问题,再追问一句 | 回答几段人话、不念内部清单;输入框下方有「回答由 AI 生成,仅供参考……」一行;iPhone 上正文右侧无大块空白、不抖;收起键盘页面不上移 | BUG-1252、1253、1255、920、内测主路径单 T2;`consult-readable-20261006.md`、`mobile-viewport-scroll-lock-20260917.md` | | | |
|
||||
| 5 | 明说要校正 | 档案出生时间是「大约」 | 新对话说「帮我做一下生时纠正」;另开一个对话说「这个解读我不认同」 | 第一句直接进入校正,不先写解读;第二句**不**进入校正,只改读法 | BUG-1306、1317;`rectification-explicit-entry-20261012.md` 第 1、1b、4、9 步 | | | |
|
||||
| 6 | 校正开场 | 第 5 条进入的校正 | 看开场三句 | 说出填的时间离最近换段差几分钟、两边哪几件事不同;离得近时有「这里连本命上升都换」;不出现秒、「记录时间是错的」 | BUG-1307、1309;`rectification-nearest-boundary-opening-20261012.md` | | | |
|
||||
| 7 | 人物档案不串人 | 人物档案里加一位家人 | 切换到家人 → 设为默认 → 回到本人 → 再切家人 | 每次显示的资料都是对应的人,名字、出生资料不串 | BUG-1030;`people-archive-p1-20260924.md` | | | |
|
||||
| 8 | 给别人出报告 | 第 7 条的家人;有点数 | 选家人 → 生成报告 → 等完成 → 打开 | 报告能生成完成(不一直排队、不失败退款),标题下有「本报告由 AI 生成,仅供参考。」 | BUG-1063、内测主路径单 T1/T2 | | | |
|
||||
| 9 | 本人报告 | 本人 | 生成一份中文完整报告并打开;切英文看一眼 | 开篇重点卡正常;英文版无中文残留;结尾免责在 | BUG-1312~1316;`report-translation-gaps-20261013.md`、`report-focus-card-20261013.md` | | | |
|
||||
| 10 | 反馈与注销 | 任一测试账号 | 对一条回答点踩并写一句反馈 → 后台「反馈」页查看;账户设置里申请注销 → 再撤销 | 后台能看到反馈;注销申请显示 7 天冷静期,撤销后账户恢复 | `surface-feedback-20260929.md`、`legal-consent-20260930.md`、`compliance-launch-20260930.md` | | | |
|
||||
| 11 | 限流与排队(容量单合入后) | 两台设备同一账号 | 1 分钟内连发 7 次提问;另找 3 个账号同时做校正 | 第 7 次得到一句「太快了,稍等一下」类提示,不扣点;同时校正的人看到「排一下队」后照常继续,不报错 | 内测容量单 T1/T2 | | | |
|
||||
Reference in New Issue
Block a user