Skip to content

[Decision] §09 要「各段时长」和「超 SLA」,但语义层算不出时间差——两问共用一个补法 #31

Description

@hotlong

Blocked-by: objectstack-ai/objectstack#16737

⬆️ 裁定 1C(维护者,2026-09-09,第 1 批):阻塞源是平台缺陷 ⇒ 去平台修。解锁判据是消费方可安装(本仓升到含该修复的版本后那条错路确实报错),⛔ 不是「上游 PR 已合并」。详见本卡 2026-09-09 的裁定评论,含一条对 1C 的公开更正:平台在飞的 PR #16778 选的补法是拒绝而非提供,所以修完之后各段时长仍然算不出来。


维护者速读

事情:设计方案第 9 章要两样东西,卡 10(#29 / PR #30)照着做时发现平台这一版给不了:

  1. 平均周转 / 各段时长——需要「两个时间戳相减」。语义层不收 SQL 也不收表达式(ADR-0021),没有地方做减法。
  2. 超 SLA——需要拿合同的 review_started_at 去比另一个对象的列(clm_contract_type.review_sla_days,九种类型分别是 2/3/5/10 天)。分析层既不能按公式过滤(§12 缺口 [Decision] Two states in DESIGN.md §03 are dead ends: a started obligation cannot go overdue, a partly paid instalment cannot be completed #10),跨对象过滤也被直接拒绝。

两问的补法是同一个:让日任务把结果盖成合同上的持久字段,之后分析层直接读那个数就行。这正是 §12 缺口 #7 已经为另一个日期运算问题选定的形状(「到期、逾期由日任务盖戳字段」),所以不是新机制。

为什么必须你拍板:字段叫什么、语义是什么,是公开契约;而且它落在卡 09(日任务那张卡)的地盘上,不是卡 10 能自己加的。

你要做的:回两个字母,比如「1A 2A」。


第一问 —— 各段时长

这里最值得你看一眼的,是它差点怎么错

开发 agent 先按直觉写了 derived: { op: 'difference', of: [avg_activated, avg_submitted] }。它不报错。它解析通过、运行成功、渲染出一个数:-0.849999999999909。

原因实测如下:

sqlite> select typeof(submitted_at), submitted_at from clm_contract limit 1;
text|2026-05-19T00:00:00.000Z
sqlite> select avg(submitted_at) from clm_contract;
2025.9166666666667          ← 平均「年份」,不是平均日期

Field.datetime 在 SQLite 上存的是 ISO 文本,AVG() 触发 SQLite 的文本→数字强制转换,取到开头的年。于是那个 -0.85 是两个平均年份之差——一个干净、合理、可以直接进仪表盘的错数。

这就是本来会发布出去的形态。 卡 10 因此改为交付「各阶段覆盖数」(已提交 60 · 已开始审查 41 · 已批准 34 · 已签署 72 · 已生效 72)和一块标明为「审批吞吐」的砖,§09 要的「本月 vs 上月」对比则用平台自己的 compareTo: { kind: 'previousPeriod' } 原语实现,没有硬编码差值。

选项

做法 代价 / 收益
1A(推荐) 在 clm_contract 上加持久的时长字段,由日任务盖戳——与 §12 缺口 #7 同形 需要 §03 加字段 + 卡 09 加一次盖戳;字段名与语义要你定。收益:§09 字面兑现,且时长可被分析层过滤与排序
1B 接受卡 10 的替代物,改 §09 的措辞为「阶段覆盖与吞吐」 零工作量,今天的板子如实。但让出一项真实的分析能力,且要改 §09
1C 向平台提需求(datediff 类度量,或 Field.datetime 改存数值时间戳),在它落地前维持替代物 一次修好所有应用;但时间不可控,期间两块砖一直是近似
1D 砍掉 contract_cycle_time 数据集 ⛔ §09 点名四个数据集,静默砍掉一个正是卡片明令禁止的。列出仅为明确否决

第二问 —— 超 SLA

卡 10 交付的是固定 30 天阈值,并把砖的标题写成「审查超 30 天」而不是「超 SLA」——因为九种类型的 SLA 是 2/3/5/10 天,30 天高于全部,且标题自己说明了阈值,读者不会误以为它是按类型的违约。

做法 代价 / 收益
2A(推荐) 并入 1A:同一个日任务顺手按类型 SLA 盖一个 review_due_at(或违约布尔),砖改为过滤持久列 一个任务、一次写入,两块砖同时不再是近似
2B 保留固定阈值,改 §09 措辞 零工作量,但「超 SLA」这项能力就没有了
2C 固定阈值作为过渡,§09 保持目标,等卡 09 落地再回头 拖延,且过渡期无期限

四棱分析(两问共用)

实际业务需求 — 偏 A。「合同平均要走多久」和「哪些审查压过了 SLA」是 CLM 买家评估流程效率时最先问的两个问题,也是对标 Ironclad/Agiloft 时销售会被追问的两个数。1B/2B 等于承认这个产品答不出来。

项目长远合理性 — 明确偏 A,理由是一致性:这个设计对完全相同的问题(日期运算算不了)已经在 §12 缺口 #7 选过一次答案,就是「日任务盖戳字段」。同一个问题给两种答案,是下一个作者最容易踩空的地方。而且持久数值列可过滤、可排序、可聚合——这正是 clm_contract 那五个汇总字段被做成 summary 而不是公式的同一个理由。

防 AI 写错 — 这一棱最偏 A,而且差距最大。今天的状态是:语义层里存在一条能跑通、能渲染、结果错的路径(op: 'difference' 那个 -0.85)。只要这条路还开着,下一个作者——人或 AI——照直觉写就会写出它,而且没有任何闸门会拦。1A 把正确答案变成一个现成的持久字段,等于把那条错路的诱因移走。1B 反而更危险:它把「这里算不出时长」写进文档,却不改变「随手一写就能得到一个假时长」这个事实。

创业阶段不扩散 — 略偏 B。1A 要加字段、改 §03、给卡 09 加活。但卡 09 本来就要建一个跑在这些对象上的日任务,所以边际成本是「一个字段加一次盖戳」,不是一套新机制。2A 更是搭同一趟车。

结论:三棱同向 A,第四棱的成本因为卡 09 已有日任务而被大幅摊薄。推荐 1A 2A。 若你更想省,1B 2B 也自洽,但必须写进 §09——沉默不可接受,理由同 #10:把矛盾留给下游卡片,就是把它推给一个更没权限、也更有压力悄悄编一个答案的位置。

⛔ 字段名与语义我没有替你定,那是公开契约。

影响

不阻塞。PR #30 本身不声称任何时长,可以按现状合并(正在做第一轮 rework,原因与本卡无关)。这两问的落地在卡 09——那张卡同时还等着 #6、#10、#14 三张决策卡,所以一并回答可以让卡 09 一次派发到位。

关联

#29 / PR #30(来源)· 卡 09(落地处,尚未派发)· #6 #10 #14(同样卡住卡 09 的三张)· DESIGN.md §09、§12 缺口 #7 与 #10。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions