Skip to content

forecast_derive_period:手填 period_start 时 period_end 按「起点 + 3 个月」推导,可越出它自己标注的季度 #1008

Description

@yinlianghui

来源:#748(文档措辞单)实施过程中,为核实「手填 period_start 的真实行为」而实测出来的行为面问题。#748 只改文档措辞、不动契约(其 option 3 明确留给维护者拍板),故行为疑点单独记录,不在那条 PR 里顺手修。

现象(实测)

src/objects/forecast.hook.tsperiod_start 由调用方给出时原样保留它(这是 #748 已确认、且现在文档已按实描述的部分),但接着用 endOfQuarter(start) = Date.UTC(y, m + 3, 0)period_end —— 这是「起点往后数 3 个月」的滚动窗口,不是「该起点所属日历季度的最后一天」。于是同一个 period_label 下窗口长度对,位置却随手填值漂移:

手填 period_start 推出的 period_end 推出的 period_label
2026-07-15 2026-09-30 Q3 2026
2026-08-15 2026-10-31 Q3 2026
2026-09-20 2026-11-30 Q3 2026

后两行标着 Q3,窗口却伸进了 Q4。period = month 时同类:起点 2026-08-17 推出 period_end = 2026-08-31,窗口只剩半个月。

实测方式:把上表三个值送进 forecast_derive_period 的真实 handler(test/helpers/hook-harness.tsmakeCtxbeforeInsert),读回 input

为什么标 finding 而不是缺陷

#748 一样,今天没有用户会踩到src/ 里没有任何写入方送手填的 period_startforecast-snapshot.flow.tscreate_forecast 只送 periodsrc/data/revenue.seed.ts 全部用 Date.UTC(y, m, 1) 算日历真值)。属于「已声明但未被行使」的路径。

不过有两点让它比 #748 的纯措辞面更值得看一眼:

  1. 入口是敞开的。 period_startrequired + notNull,并且出现在记录表单的 Snapshot 区块(src/views/forecast.view.ts:127)。source: 'manual'(经理手工录入)正是文档写明的三种来源之一,走的就是这个表单。
  2. 二级后果(未实测,只是从元数据推出的): 夜间 sweep 用 period_start <= today <= period_end 选「当前季度那一行」(src/flows/forecast-snapshot.flow.tsCURRENT_PERIOD_FILTER)。一条手工建的、窗口恰好罩住今天的行会满足这个条件,于是 sweep 可能把它当成本季度行去 write_snapshot(覆盖四个金额、把 source 改成 scheduled),并因此认为本季度行已存在而不再开日历真值的那一行。

可选修法(供 triage 参考,不建议由文档单顺手带)

  1. period_end 改为按 period_label 所属的日历周期算(即先 startOfPeriod(period, start)endOfQuarter)—— 窗口回到日历真值,但 period_start 仍是手填值,行内自相矛盾只是换了个位置;
  2. 手填的 period_start 一并吸附到日历边界(forecasting.mdx 的「boundary is always computed, never typed」措辞,对手填 period_start 的情形是过度承诺 #748 的 option 3)—— 改契约,会让「给某个特定季度拍快照」以外的用法失去表达能力;
  3. 拒绝而不是接受:period_start 不是所属周期第一天时报校验错。对 AI 写元数据/写记录的场景,这是「声明即强制」方向上最不容易出错的一种,但同样是契约变更。

三者都动 crm_forecast 的写入契约,需维护者拍板。严重度请 PM 在 triage 轮判定。

Refs #748 #530 #590

Metadata

Metadata

Assignees

Labels

backendServer-side behaviour — hooks, flows, actionsbugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions