Skip to content

Forecast has no writer — implement the scheduled snapshot flow the object docs promise #590

Description

@os-zhuang

Found during a full business/platform-capability review of the app (2026-08-02). This is the highest-value functional gap that needs no platform work.

Problem

src/objects/forecast.object.ts:14-16 documents that forecast snapshots are produced by a "forecast.hook.ts scheduled job (default: nightly)". That job does not exist: forecast.hook.ts contains only forecast_derive_period, a beforeInsert/beforeUpdate field-derivation hook. No flow, hook, or job anywhere in src/ ever creates a crm_forecast row.

Consequence: the entire forecasting story — the forecast object, its attainment_pct/coverage_ratio formulas, the forecast_metrics dataset, and the quota-attainment dashboard table — runs on the 3 hand-seeded rows in src/data/index.ts. On any live environment the feature is an empty shell.

Proposed shape

A scheduled flow (weekly or nightly — consistent with the object's own doc comment) that:

  1. Enumerates active opportunity owners (users owning at least one open or current-period-closed opportunity).
  2. For each owner + current period, aggregates from crm_opportunity: pipeline (open), best_case / commit (by forecast_category), closed_amount (won in period).
  3. Upserts on (owner, period) so re-runs are idempotent — same discipline as contract_renewal / opportunity_stagnation.
  4. Leaves quota untouched (manually maintained until a quota model exists — separate backlog item).

Follow the repo's sweep conventions: idempotency gate, runAs, and a runtime test in the designated runtime-coverage file (the runtime-coverage.test.ts ratchet will demand one anyway).

Acceptance

  • Fresh install + one sweep run ⇒ every active opportunity owner has a current-period crm_forecast row with non-trivial pipeline numbers.
  • Re-running the sweep does not duplicate rows.
  • forecast.object.ts doc comment matches the implemented schedule.

Activity

  1. added
    enhancementNew feature or request
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 2, 2026
  2. self-assigned this
    on Aug 2, 2026
  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮
    会话:session_019SS7C5SXpniKeCApxgARyf
    分支:claude/issue-590-forecast-snapshot
    Worktree:hotcrm-issue-590


    Generated by Claude Code

  4. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 复核:通过,已入队合并 — #615

    核验依据(以 GitHub 为准,不采信报告自述):

    • PR feat(forecast): add the forecast_snapshot nightly writer (#590) #615 为 draft、目标 main、正文以 Fixes #590 开头
    • 9 项检查全绿(Build and Test 22.x / Quality Checks / Check Changeset / link-check / CodeQL / Analyze Code / Playwright / Label PR / Vercel)
    • 变更 9 个文件,全部在范围内:新增 src/flows/forecast-snapshot.flow.ts、src/flows/index.ts 注册、forecast.hook.ts + forecast.object.ts、test/flow-scheduled.test.ts + test/helpers/flow-harness.ts、两处文档、一份 changeset。无 content/docs/releases/ 改动,无无关文件

    交付内容:每晚 03:00 的 forecast_snapshot 调度 flow(runAs: 'system'),为每一位持有活跃商机的负责人 upsert 一条当季 crm_forecast 快照,按存储列 forecast_category 做累进分桶(pipeline ⊇ best_case ⊇ commit),quota 永不写入。期间边界交给 forecast.hook.ts 派生 period_start——flow 模板表达不了"本日历季度第一天",由对象派生使 #530 钉住的日历对齐不变式对运行期快照同样成立。best_case_amount / commit_amount 的字段描述改为它们真正度量的东西(原先写的概率阈值没有任何 writer 执行,且与 stage→category 映射矛盾)。

    10 个 runtime 用例跑真实 AutomationEngine(非 PENDING_FLOWS 豁免),并做了反向验证:把 best-case 过滤器从 $in: ['best_case','commit'] 改成 'best_case' 后测试如期失败,断言不是空转。

    派生发现已另开 issue,未夹带进本 PR:#613(forecast 种子 externalId: 'period_label' 在按人快照后不再唯一)、#614(quota_attainment_by_rep 缺期间过滤导致跨期间求和)。两者将在下一轮评估是否入队。


    Generated by Claude Code

  5. added
    priority:p0Critical: blocker, must ship before MVP
    and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p0Critical: blocker, must ship before MVP

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions