Skip to content

fix(leads): 一个 hot 定义 —— Hot Leads 视图阈值对齐 lead_assignment 路由流(rating ≥ 4) - #774

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-766-hot-leads-threshold
Aug 5, 2026
Merged

yinlianghui merged 2 commits into
mainfrom
claude/issue-766-hot-leads-threshold

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #766

按分诊裁定执行:仓库里只留一个 hot 定义,语义生产者是 lead_assignment 路由流(它的判定有用户可见后果:1 天 SLA + 「24 小时内认领」告警),hot_leads 视图跟随它。

复核结论:issue 的四条前提全部成立(基线 origin/main = 440db01)

前提 实测
视图切在 4.5 src/views/lead.view.ts:358 —— { field: 'rating', operator: 'greater_than_or_equal', value: 4.5 }
流切在 4 src/flows/lead-assignment.flow.ts:127 边 e2 —— has(record.rating) && record.rating != null && record.rating >= 4,命中后 sla_hot(TODAY() + 1)+ notify_hot(inbox + email,标题「Hot lead — assign within 24h」)
评分只有整星 src/objects/lead.hook.ts:146-148 —— Math.round(clamped),注释「round to WHOLE stars」;字段是 Field.rating(5)(lead.object.ts:200),控件只给整星
auto_flag_hot_lead 是幽灵 全仓库 grep 只有那句注释自己一处命中,没有任何同名 flow / hook / action

没有找到任何支持 4.5 的书面理由,反而找到一条相反的记录:src/data/sales.seed.ts:473-478 明确写着「a seeded 4.5 was the one rating in the system the contract could never produce, and it rendered as the broken half-star it was meant to prevent (#591)」—— 半星值在种子数据里已经因为同一个原因被删过一次。裁定的前提成立,按 A 方向实施。

改了什么

  • src/views/lead.view.ts:hot_leads 过滤阈值 4.5 → 4;幽灵 workflow 注释删除,换成真实生产者(lead_assignment 流的 check_hot 决策 / 边 e2)的引用,并写清整星字段上半星阈值等价于 == 5 这件事。high_priority、其余视图、流本身都没动。
  • test/hot-lead-threshold-parity.test.ts(新):钉住「视图阈值 == 路由流 hot 分支阈值」。
  • content/docs/sales/leads.mdx + zh-Hans / zh-Hant:只改 Hot Leads 与 High Priority 两行。
  • .changeset/hot-leads-one-definition-of-hot.md。

与 High Priority 的重叠(issue 明确要求回答)

会重叠,而且是完全重叠:改后两个视图的过滤条件逐字相同(rating ≥ 4 + status in (new, contacted)),一条四星线索现在同时出现在两个视图里。

这是「只留一个 hot 定义」的代价,不是疏漏,本 PR 选择明写而不是靠调阈值把它藏起来:

两个视图的区别在用途,不在人群,注释与文档都按这个口径写:

视图 是什么 列 / 排序
🔥 Hot Leads 工作队列 —— 就是路由刚刚盖了 SLA、发了告警的那批,从上往下打 电话、邮箱、负责人、下次跟进日期;按下次跟进日期升序
High Priority 速览表 —— 看分布和来源,不是工作顺序 线索来源;行按评分着色(五星绿、四星橙);无 SLA 列、无时间排序

「两个视图是否还该并存」是产品决策,不在本单裁定范围内,已另立 finding 单(见下),本 PR 不删不改 high_priority。裁定里那句「五星队列如果是真实需求应以自己的名字另立视图」也写进了代码注释,避免下一个人再往 Hot 上加半星。

守卫:两侧阈值都从元数据派生

test/hot-lead-threshold-parity.test.ts(16 个断言)不比较源码字面量,两个数都是从编译后的 stack 里取出来的:

  • 视图侧:stack.views[…].listViews.hot_leads.filter 里 field === 'rating' 的那条子句(operator + value);
  • 流侧:lead_assignment 中 target === 'sla_hot' 的 conditional 边,从它的 CEL source 里取排序比较(>= / > 及其数值;!= null 这类总性守卫不计入)。

三层断言:

  1. 阈值与运算符一致(greater_than_or_equal ↔ >= 有显式映射表,未知运算符直接抛,不静默放过)。
  2. 阈值必须是整星 —— 这一条同样是派生的:先断言 lead.hook.ts 里 Math.round(clamped) 还在(是它让评分只有整星),再要求阈值是 1..5 的整数。失败信息直说「在整星字段上切 4.5 实际等于 5」。
  3. 真值表:对 rating = 1..5 / null / 键缺失,一边交给真正的 AutomationEngine.evaluateCondition(traverseNext 判定边用的同一个方法)问「路由是否判 hot」,另一边把 shipped 的视图 filter 跑一遍问「是否在队列里」,要求两个答案逐行相同。

反向真空(这些断言是让上面三层不至于空转的部分):视图 rating 子句数量必须恰好 1、流的 rating 排序比较必须恰好 1、两个阈值必须是 number、真值表里 hot 与非 hot 都必须出现(否则「全 false 对全 false」也能全绿)、视图状态过滤必须包含 new(否则流在入库时判 hot 而队列永远为空,是换了个形式的同一个缺陷)。

这个文件不钉 high_priority(第二个 commit 一并说明):改后它与 hot_leads 过滤相同,唯一可写的断言就是「两者相同」,而那是在给一个还没做的产品决策上锁 —— #775 的任一结论(合并,或用非评分判据重新差异化)都会让它无故变红。有生产者背书的不变量只有「流 ↔ 视图」这一条。

另外一条独立断言处理幽灵注释:把 src/views/*.view.ts 里所有「snake_case 名 + flow/workflow/automation」(两种词序)的引用扫出来,要求每个名字都能在 stack.flows / hooks / actions 里解析到。auto_flag_hot_lead 这个死名因此从源码里彻底删掉(没有保留在散文里),否则它会命中自己写的守卫。

反向验证(方向是先预测再跑的)

预测:这是普通的红,不是反转方向 —— 两侧都从活元数据读,没有「规则本来就没在跑」的情形。

把视图阈值改回 4.5(只改这一处),预测红的三条,实际红的正是这三条:

× the view and the routing flow cut at the same rating, with the same operator
  → expected { op: '>=', threshold: 4.5 } to deeply equal { op: '>=', threshold: 4 }
× the cut is a whole star, because rating only ever holds whole stars
  → rating is a whole-star field, so a cut at 4.5 silently means 5 — write the star you mean
× rating 4: routed-hot and in-queue agree
  → rating 4 is routed HOT (1-day SLA + "assign within 24h" alert) but is NOT in the 🔥 Hot Leads queue
Tests  3 failed | 13 passed (16)

把幽灵注释改回 (Set by the auto_flag_hot_lead workflow.):

× every automation a view comment names is registered in the stack
Tests  1 failed | 15 passed (16)

两次都已还原。

文档同步(#768 刚落地的那三页)

#768 是按旧阈值如实描述的(「实际上的五星队列,四星在 High Priority」),这个描述随本 PR 失效,因此同 PR 顺改,只动这两行 × 三个语言:

  • Hot Leads:改成「四星及以上 —— 正是新线索路由判为热、给出次日跟进日期、并向负责人发出 24 小时内认领告警的那一批」;
  • High Priority:改成「同一批线索,只是摆成速览表而不是工作队列」,保留着色与线索来源的描述。

leads.mdx:132 那条提示(「四星及以上是明天,其余三天后」)说的是流的 SLA,本来就是对的,没动。

验证

pnpm hygiene    ✓ source hygiene clean(242 files;含 no raw control bytes)
pnpm typecheck  ✓ tsc --noEmit 无输出
pnpm validate   ✓ exit 0(14 Views / 24 Flows;仅既有的 approval / campaign_member 警告)
pnpm lint       ✓ exit 0(13 warning / 14 suggestion,均为既有项)
pnpm build      ✓ exit 0 → dist/objectstack.json (1913.4 KB)
pnpm test       Test Files 63 passed (63) / Tests 1495 passed | 1 skipped (1496)

新增文件单跑:Tests 16 passed (16)。另用 grep -naP 对本 PR 全部改动文件做了控制字符自查,干净。

第二个 commit(test(leads): drop the unused high_priority binding…)清掉 CodeQL 报的 Unused variable highPriorityView:删掉而不是补一条断言把它用起来 —— 唯一可写的断言是真空/越权的那种(见上)。断言数量前后都是 16,pnpm typecheck 与全量 pnpm test(1495 passed | 1 skipped)在该 commit 上重跑过。

边界

平台版本保持 17.0.0-rc.2,未升级任何 @objectstack/*。未触碰:src/flows/lead-assignment.flow.ts(流是生产者,本次让视图跟随它)、src/views/opportunity.view.ts 与 dashboards / seeds 注释(#744 在飞)、content/docs/sales/activities.mdx(#747 / #771 在飞)、content/docs/releases/、high_priority 视图本身、四个语言包(视图 label 未变)。

顺带发现(已另立单,本 PR 不修)


Generated by Claude Code

… routing flow (#766)

The `hot_leads` list view cut at `rating >= 4.5` while the `lead_assignment`
flow's hot branch (edge `e2`) cuts at `>= 4`. `rating` is a whole-star field —
`lead.hook.ts` rounds the computed score, `Field.rating(5)` renders as a star
widget with no half steps, and the one seeded 4.5 was deleted for the same
reason (#591) — so `>= 4.5` was `== 5` on every row that can exist.

A 4-star lead was therefore routed hot, stamped with the 1-day SLA, and its
owner alerted "Hot lead — assign within 24h", while the queue that alert points
at could never contain it.

The flow is the semantics producer (its verdict has user-visible consequences),
so the view follows it: threshold `>= 4`. The comment crediting an
`auto_flag_hot_lead` workflow is removed — no such automation exists in this
repo — and replaced with a reference to the real producer.

`test/hot-lead-threshold-parity.test.ts` derives both thresholds from the
shipped metadata and pins their agreement, including a truth table that asks
the real AutomationEngine and the real view filter, for every rating a lead can
carry, whether "routed hot" and "in the queue" give the same answer.

Docs: the Hot Leads / High Priority rows on leads.mdx and its zh-Hans /
zh-Hant siblings described the pre-fix behaviour (#768 wrote them against the
old threshold) and now match what ships.

Fixes #766

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa
@vercel

vercel Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 5, 2026 6:48pm

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces labels Aug 5, 2026
Comment thread test/hot-lead-threshold-parity.test.ts Fixed
CodeQL flagged `highPriorityView` as unused, and it was: the file compares the
hot_leads threshold against the lead_assignment hot branch, and high_priority
carries no part of that invariant.

Removed rather than given an assertion to justify it. Post-#766 high_priority
holds the same filter as hot_leads, so the only assertion available would pin
that sameness — a guard over an open product decision (#775: merge the two
views, or re-differentiate high_priority on a non-rating criterion), either
outcome of which would turn it red with nothing broken. A comment now records
why the binding is absent, so the next reader does not re-add it.

No assertion changed: 16 tests before and after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

3 participants