报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。 Co-Authored-By: Grok (agent loop)
11 KiB
PROGRESS · 契约与小工具去重 · 2026-10-13
分支 codex/contracts-helpers-dedupe-20261013,基线 origin/staging cd7e0a34933bd288ad7a529d51477c7099e1f6b1。只搬定义和引用。哈希字节、状态字符串、主题枚举值不变。没有改表、迁移、依赖、page.tsx、scripts/jyotish_api_server.py。没有写 BUG_HISTORY、CHANGELOG、DESIGN.md。docs/agents/domain.md 与 CONTEXT.md 里没有 EvidenceDomain,未加术语句。本分支 BUG 最大号 1357,本单没有新缺陷,不占新号。
做了什么
- T1:报告创建请求的 zod、默认主题、深度枚举收进
frontend/src/lib/contracts/personal-report-request.ts。服务端类型用z.infer,客户端发送类型用z.input。原导出名在 route-core、plan、contract 里 re-export。按钮删掉手写 interface。 - T2:校正状态列表与入口类型收进
frontend/src/lib/contracts/rectification-entry.ts。线上形状的 status 仍是string。服务端类型在此之上收窄。case-status.ts再导出这些数组,import 无环。客户端解析器仍接受任意字符串 status。 - T3:旧引导流程的
EvidenceDomain/evidenceDomains改名为LegacyGuidedEvidenceDomain/legacyGuidedEvidenceDomains(数组里的字符串没变)。出生时间证据的CandidateResult改名为LegacyCandidateResult。ChoiceKey只留在probe-explain.ts(choice-card.ts已经 import 它,反向会成环),choice-card 再导出。ClaimStatus只留在personal-report-contract.ts。v9 的 EvidenceDomain 与快照的 CandidateResult 没改名。 - T4:同义的对象判断和有限数判断收进
frontend/src/lib/guards.ts。sha256Hex收进frontend/src/lib/hash.ts(仅服务端)。长文、兑换码、消息正文改为调用它。
替换清单(文件 → 共享函数)
调用处多数保留原来的局部名字,用 import 别名接到共享函数。原因见文末「偏离」。别名不是另一份实现。
| 共享函数 | 文件 |
|---|---|
isRecord |
generate-personal-report-button.tsx、personal-report-page.tsx、account-deletion.ts、skill-package-registry.ts、report-public-projection.ts、compose-receipt.ts |
asRecord(调用名本来就是 asRecord) |
varga-observations.ts、refinement-packet.ts、inference-adapter.ts、confirmation-gate.ts、ephemeris-view.ts、daily-starlanguage.ts、consultation-step-timing.ts |
asRecord(别名 recordOf) |
subject-birth.ts、consultation-subject-resolver.ts、chart-subject-write.ts |
asRecord(别名 record,非数组否则 null) |
birth-profile-timezone.ts、chart-view-mapper.ts、consultation-route-service.ts、general-daily-panchanga.ts、personal-report-longform-generate.ts、rectification-adopted-chart-tiers.ts、reply-quality-capture.ts、report-birth-binding.ts、v9/engine-client.ts、v9/segment-contract.ts、personal-report-route-core.ts、personal-report-worker.ts、personal-report-generation.ts |
recordOrEmpty(别名 record,非数组否则 {}) |
consultation-workflow.ts、chart-birth-truth.ts、consultation-evidence-card.ts、consultation-judgment-table.ts、admin/rectification-telemetry-summary.ts、report-fact-table-values.ts、report-fact-tables.ts |
finiteNumberOrNull(别名 finiteNumber) |
personal-report-route-core.ts、personal-report-worker.ts、personal-report-generation.ts、rectification-candidate-result.ts |
sha256Hex |
personal-report-generation.ts 再导出;hashLongformMarkdown、hashRedeemCode、messageContentHash 改为调用它 |
未合并及原因
| 留下的私有函数 | 原因 |
|---|---|
rectification-candidate-result.ts 的 function record |
数组也当对象,否则 null。与 asRecord 不同义 |
geoapify-location-service.ts 的 function record |
数组也当对象,否则 {} |
server-owned-birth-profile.ts 的 function finiteNumber |
接受数字字符串,失败返回 undefined |
chart-profiles/route.ts、chart-profiles/[id]/route.ts、synastry-reports/route.ts、admin/admin-error-response.ts 的 errorMessage |
三种错误提取方式,任务书要求不并 |
personal-report-service-core.ts 的 PERSONAL_REPORT_DEPTHS |
名字和用途都不是 T1 的 REPORT_DEPTHS,没有硬合 |
任务书 §2.3 把「含数组否则 null」记在 rectification-adopted-chart-tiers.ts。当前 staging 上该函数已经排除数组,所以并入 asRecord。真正含数组否则 null 的那份在 rectification-candidate-result.ts,保留。
断言
| 原值 | 新值 | 原因 |
|---|---|---|
workflow.indexOf("function record(") |
workflow.indexOf("function rectificationBoundaryFromGate(") |
非数组否则空对象的私有 record 已删除。切片终点改到紧随其后的既有函数。锁住的 schema 字段没改,chart: z.record(z.unknown()) 仍在 |
文件:frontend/tests/consultation-workflow-contract.test.ts。其余点名测试没有改断言。
仅 import
这些文件只改了 import 行,esbuild 转译结果与 staging 相同:
rectification-choice-card.tsx、rectification-message-entry.tsx、use-rectification-case-snapshot.ts、answer-choice.ts、block-scan.ts、choice-action.ts、turn-intent-classifier.ts、turn-question.ts、window-widen.ts、rectification-chat-choice-run.ts。
esbuild 差分
对 frontend/src 里每个改动文件,用 esbuild transform(.ts / .tsx,format=esm,不打包)对比 origin/staging 与工作区。结果:相同 26,有差异 49,新文件 4,转译失败 0。
新文件:contracts/personal-report-request.ts、contracts/rectification-entry.ts、guards.ts、hash.ts。
有差异的原因,按文件:
| 文件 | 原因 |
|---|---|
generate-personal-report-button.tsx |
手写请求类型删除,默认主题改为从 contracts 取值 |
personal-report-page.tsx、account-deletion.ts、compose-receipt.ts、report-public-projection.ts、skill-package-registry.ts |
本地 isRecord 删除,改为 import |
consultation-step-timing.ts、daily-starlanguage.ts、ephemeris-view.ts、confirmation-gate.ts、inference-adapter.ts、refinement-packet.ts、varga-observations.ts |
本地 asRecord 删除,改为 import |
subject-birth.ts、consultation-subject-resolver.ts、chart-subject-write.ts |
本地 recordOf 删除,import asRecord 并别名为 recordOf |
birth-profile-timezone.ts、chart-view-mapper.ts、consultation-route-service.ts、general-daily-panchanga.ts、personal-report-longform-generate.ts、rectification-adopted-chart-tiers.ts、reply-quality-capture.ts、report-birth-binding.ts、engine-client.ts、segment-contract.ts |
本地「非数组否则 null」的 record 删除,import asRecord 并别名为 record |
consultation-workflow.ts、chart-birth-truth.ts、consultation-evidence-card.ts、consultation-judgment-table.ts、rectification-telemetry-summary.ts、report-fact-table-values.ts、report-fact-tables.ts |
本地「非数组否则空对象」的 record 删除,import recordOrEmpty 并别名为 record。z.record(...) 没改 |
rectification-candidate-result.ts |
只把「有限数否则 null」改为 import。含数组的 record 仍在文件里 |
personal-report-route-core.ts、personal-report-worker.ts |
同上,再加上 schema / 调用处的 guard import |
personal-report-generation.ts |
guard 搬家,并且 sha256Hex 改为从 hash.ts 再导出 |
personal-report-longform-appendix.ts、supabase/codes.ts、message-origin.ts |
哈希函数体改为调用 sha256Hex。输入仍按 utf8 |
personal-report-plan.ts、personal-report-contract.ts |
REPORT_DEPTHS 字面量搬到 contracts,本文件 re-export |
case-status.ts |
状态数组搬到 contracts,本文件 re-export。数组内容相同 |
rectification-entry.ts |
本地状态列表删除,Set 改用共享常量 |
birth-time-question-planner.ts、birth-time-journey-turn.ts、birth-time-journey-transitions.ts、birth-time-journey-turn-persistence.ts、birth-time-journey-case-loader.ts |
运行时常量改名 legacyGuidedEvidenceDomains。不打包,所以导出名出现在转译结果里。数组里的字符串没变 |
相同的 26 个文件里,上面「仅 import」10 个只动了 import。其余是类型改名或类型再导出,类型擦除后转译结果与 staging 相同(含 open-request.ts、choice-card.ts、report-evidence-bundle-v2.ts、birth-time-evidence.ts 及其余 Legacy* 引用)。
测试
开工没有跑全量 npm test(本轮不做全量,也没有新的全量基线数字)。完工数字:
| 检查 | 命令 | 结果 |
|---|---|---|
| 类型 | frontend 下 tsc --noEmit -p . |
0 错 |
| 规范 | frontend 下 npm run lint |
0 错,123 条警告。警告是原来就有的。本轮删过 personal-report-worker.ts 里不再使用的 JsonRecord,没有留下新警告 |
| 改到的测试与任务书点名的测试 | node node_modules/tsx/dist/cli.mjs --test(guards、报告入口、首页校正解析、校正契约、报告生成与长文哈希、咨询 workflow 契约、出生时间改名相关) |
209 通过,0 失败,0 取消,0 跳过 |
没有跑前端全量 npm test、Python 快速门、test:db。supabase/codes.ts 只改了哈希函数体,没有改查询、表或迁移。
偏离
- 调用处没有改成共享函数名,改用 import 别名。把调用处全部改名时,已跟踪 diff 为 621 增、852 删(合计 1473),加上当时的新文件 248 行,约 1721 行,超过硬红线 1500。别名之后已跟踪 diff 为 229 增、461 删。共享函数仍只有
guards.ts/hash.ts里的那一份。 - 报告按钮是客户端组件,但它要取值
DEFAULT_REPORT_THEMES,而这份常量和 zod schema 在同一个文件。因此这个按钮会把 zod 带进自己的包。任务书允许「zod 已在该组件 bundle 内」的例外;首屏 gzip 由验收侧看。测试仍从按钮 re-export 引用该常量。 LegacyCandidateResult的引用比任务书点名的文件多(向导、旅程加载等)。不改的话tsc会报错,所以一并改了类型名,没有改运行时值。- 含数组变体的文件名与任务书 §2.3 不一致,以当前 staging 的函数体为准,见上表。
完工 diff:已跟踪 229 增 + 461 删,新文件与本记录 345 行,合计 1035,低于 1500。