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

  1. added a commit that references this issue on Sep 8, 2026
  2. zhuangjianguo commented on Sep 9, 2026

    @zhuangjianguo
    Collaborator

    前提刷新 —— 平台侧根因已有卡,且在等维护者动作。 ⛔ 不是裁决,不动标签。

    本卡第一问的实测(avg(submitted_at) 返回 2025.9166666666667,derived: { op: 'difference' } 渲染出 -0.849999999999909)在上游已经立案:

    objectstack-ai/objectstack#16737 — service-analytics: AVG() over a Field.datetime measure returns SQLite's text→numeric coercion (an average YEAR) with no error, and derived: { op: 'difference' } renders the difference of two of them as a clean plausible number —— open,且带 pm:awaiting-maintainer(决定已做,只剩一次 GitHub 之外的人工动作)。

    同族另有两张 open:#16099(无任何层拒绝不自洽的聚合/字段类型配对,avg over datetime 在 SQLite 能跑、在 Postgres 报错)与 #15768(datetime 度量在分析响应里被定型为 number)。

    这对本卡的意义:选项 1C(向平台提需求,等它落地前维持替代物)在写这张卡时是「时间不可控」的假设面,现在它有具体载体和状态了。⇒ 裁本卡之前应先读 #16737 的现状 —— 如果平台给出日期差度量或修掉静默强制转换,1A 的「日任务盖戳字段」就不是唯一出路,1C 的成本估计也要重算。

    另:本卡与 #11 #19 #20 #28 同属前提取自 @objectstack/* 17.3.0 的一批。#35 已把本仓推到 17.4.0 并于今日合并(PR #37,origin/main @ 5d5a254,锁文件零 17.3.0 残留)。⇒ 这一批的平台能力断言现在都该在 17.4 上重跑再定,本卡的语义层限制(ADR-0021 不收 SQL/表达式、跨对象过滤被拒)尤其需要重验。

    取数时刻 2026-09-09T14:5xZ。


    Generated by Claude Code

  3. zhuangjianguo commented on Sep 9, 2026

    @zhuangjianguo
    Collaborator

    裁定:1C + 2B —— 去平台修;本仓维持替代物并把措辞改成如实

    维护者裁决,2026-09-09,常设决裁批第 1 批,逐字:

    第 1 批 平台的问题去平台修,业务的问题按照你的意见。

    本卡的阻塞源是平台缺陷(语义层不做日期减法;AVG() 打在 datetime 上返回平均年份)⇒ 走「去平台修」。这也与决策分析写法的应用仓推荐序特例一致:阻塞源是平台缺陷 ⇒ 恒荐等待;⛔ 不荐绕行(形状容错、复刻平台规则)、不荐劈半落地。⇒ 1C(等平台)+ 2B(保留固定阈值,措辞如实)。

    ⛔ 因此 1A / 2A 明确不采纳:用每日任务在应用里补出分析层该有的日期运算,正是该特例点名的「复刻平台规则」。

    ⚠️ 公开更正:我呈报时对 1C 的描述被一个更晚的读数证伪

    呈报时我把 1C 写成「一次修好所有应用」。录裁前重读平台侧,这句话不成立:

    • 平台卡 objectstack-ai/objectstack#16737 — open,bug · priority:p2 · domain:services · pm:awaiting-maintainer,assignee os-trump。
    • 它已经有一个 open PR:objectstack-ai/objectstack#16778,标题逐字:

      feat(service-analytics)!: refuse an aggregate a datetime measure's field type cannot carry, and reconcile the storage-form annotations to one measured statement

    ⇒ 平台选定的补法是拒绝,不是提供。#16778 合并后,本仓依然算不出各段时长 —— 变化的是:那条「能跑通、能渲染、结果错」的路径会开始响亮报错,而不是给出 -0.849999999999909 这种可信的假数。

    洞被堵上了,能力没拿到。 这对本卡的意义:

    • 防错轴的紧急项(静默假数)由 #16778 解决 —— 那是本卡真正危险的部分,它有归宿了。
    • 第 9 章要的「各段时长」在可预见的将来仍然给不出,除非平台另有一张卡提供日期差度量原语。#16737 的验收清单第 3 条明说「Either it errors, or it returns something a reader cannot mistake for a duration」—— 两条路都不产出时长。

    ⛔ 我不静默重裁。裁定按字面执行为 1C + 2B。但若你看了上面这段觉得「等一个只会报错的修复」不是你要的意思,说一句,本卡回决策箱重开选项(那时真正的选项是:向平台立一张日期差度量原语的能力卡,还是接受第 9 章永久少这两个数)。

    裁后执行

    1. 状态转换:needs-user-decision → pm:blocked,正文加机器可读行 Blocked-by: objectstack-ai/objectstack#16737(同一笔标签写入)。
    2. §09 措辞如实(随折叠的设计文档 PR,链首 [Decision] DESIGN.md §03 requires a termination reason but declares no field for it #6,本卡为 Refs #31 的搭车项,⛔ 非 Fixes):第 9 章不再无条件承诺「各段时长」与「超 SLA」,改为记明现状 —— 交付的是阶段覆盖数与审批吞吐,两个原指标等平台的日期能力,并点名 #16737。沉默地留着一个兑现不了的承诺,正是本卡自己反对的形状。
    3. 卡 09 的 F3(法务 SLA 巡检)按 2B 写:固定阈值,且砖的标题必须自带阈值(「审查超 30 天」而不是「超 SLA」),因为九种合同类型的真实 SLA 是 2/3/5/10 天,30 天高于全部 —— 标题自陈阈值,读者就不会误读成按类型的违约。
    4. 解锁判据:#16737 关闭时本卡回队重验,⛔ 判据不是「PR 合并」而是「本仓升到含该修复的版本后,那条错路确实报错」。跨仓解锁判据是消费方可安装。
    5. 已在平台卡上留下本仓作为具名下游消费者的记录。

    Generated by Claude Code

  4. zhuangjianguo commented on Sep 10, 2026

    @zhuangjianguo
    Collaborator

    席位裁定 —— 解锁判据未满足,pm:blocked 保持

    #80 在本仓实际安装的版本上重跑了这张卡的探针。判据没有满足,而且这张卡的措辞刚好证明了它自己的价值。

    实测(17.4.0,54 个 @objectstack/* 包全部 17.4.0,better-sqlite3 13.0.3,从 node_modules 读出)

    那条错路三层都不拦:

    层 结果
    作者期 validate / lint / typecheck 全部退出 0,derived 原样编进 dist/objectstack.json
    查询构建 POST /api/v1/analytics/dataset/query → HTTP 200,回显 AVG(submitted_at)
    执行 cycle_days = -0.8555555555556111,avg_submitted = 2025.8833333333334

    操作员看到的是两块普通指标砖:-0.86 / Avg Cycle (days?) 和 2,025.88 / Avg Submitted —— 没有报错横幅、没有空态、没有校验提示、控制台无错。

    这是本卡列出的三种结局里的第三种,也是本卡认定最危险的那种:它照样撒谎。 数值与本卡原记录的 -0.849999999999909 不同,只因演示数据的日期随启动日漂移;机制与行数(120 行 / 60 已提交 / 72 已生效)完全一致。

    ⇒ 本卡「⛔ 不得用应用侧日任务盖戳」与 §09 已记录的交付面全部不变。pm:blocked 保持,⛔ 不进决策箱,⛔ 不需要维护者现在做任何事。

    ⚠️ 上游卡已关闭,而没有任何已发布版本带着那个修复

    这是本轮最该记下的一件事:

    • objectstack-ai/objectstack#16737 已于 2026-09-10 06:13Z 关闭(completed)。

    • @objectstack/service-analytics 的 latest 就是 17.4.0 —— npm 上 17.x 之后没有更新的发布版。

    • 而 17.4.0 自己的 CHANGELOG.md 白纸黑字写着这个洞是开的(我自己拉的包,逐字核过):

      sum / avg over a temporal column are refused by no layer and answered by the backend (an epoch mean on SQLite, an error on Postgres) … a derived measure is numeric by construction, because computeDerived coerces its operands with Number().

    这正是本卡当初把判据写成「消费方可安装」而不是「上游 PR 已合并」的理由。 那句 ⛔ 今天兑现了:上游卡关了,本仓装的版本行为一个字节没变。判据措辞是对的,⛔ 不要改它。

    一条上游发版说明里的新事实,比本卡原有的更细

    洞是按后端分岔的:SQLite 上是 epoch 均值(静默假数),Postgres 上是报错。 也就是说这个陷阱是演示环境(SQLite)专属的 —— 真正跑在 Postgres 上的部署会直接报错。本卡原文只写了「⛔ 未在第二种方言上复核」,现在平台自己的发版说明把答案写出来了。这对将来落地 1A/2A 时怎么描述这个字段有用,⛔ 但不改变今天的裁定。

    ⚠️ 一个容易看错的地方,记下来免得下次踩

    17.4.0 确实发了这一族的两个兄弟修复(min/max 的类型描述),但没发「拒绝」那一半:装好的包里搜不到 16737 也搜不到 16778。skim 一眼 temporal 相关的发版说明,很容易把兄弟当成本体。 下一次读 #16737 状态的人请先看 latest 的实际行为,不要只看 issue 是不是 closed。

    已做

    上游 #16737 上补了一条消费方读数(latest 上行为未变 + 它自己 CHANGELOG 的原话),⛔ 措辞是「请问哪个发布版会带上」,不是「你关错了」—— 修复很可能已在 main 上,只是还没发版。

    附:本卡引用的 17.3.0 测量已刷新

    src/datasets/cycle-time.dataset.ts 注释里那段标着 17.3.0 的测量,现在有了 17.4.0 的同形读数:形状相同、数字更新、结论不变。⛔ 那条注释本身不改(#80 明令禁止),刷新后的读数落在 #80 的报告里。顺带一提:正因为那条注释老老实实标了版本号,这个缺口才看得见 —— 一条不标版本的测量会一直被当成当前事实。


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    Contributor

    Seat note · 2026-10-09T14:41Z — waiting state, written on the card.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    Contributor

    Seat note · 2026-10-09T16:08Z — the 17.7.0 reading exists.

    #82's dev re-ran #80's probe on 17.7.0 (PR #85, section #31's unlock probe on 17.7.0): the wrong path now errors at every door — os validate / os build exit 1 (measure-aggregate-field-type-refused ×2), PUT /api/v1/meta/dataset/probe31_cycle → 422 INVALID_METADATA with nothing stored, POST /api/v1/analytics/query avg(submitted_at) → 400 INVALID_FIELD (query was NOT run). Probe never committed; tree restored and proved by state.

    This card's criterion is 「本仓升到含该修复的版本后那条错路确实报错」 — it is met on the branch, and becomes this repo's state when PR #85 merges (main is still on 17.4.0 until then). At that merge the seat brings this card back to the maintainer: 1C's wait is over, and as this card's own correction recorded, the platform fixed by refusing, not by providing — the per-stage durations remain uncomputable on 17.7.0.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    Contributor

    解锁判据已成立——请决定本卡怎么收尾 · 2026-10-10T00:17Z

    Governing text: 本卡裁定 1C(维护者,2026-09-09,第 1 批)「阻塞源是平台缺陷 ⇒ 去平台修。解锁判据是消费方可安装(本仓升到含该修复的版本后那条错路确实报错)」· DESIGN.md §09

    维护者速读

    事情:PR #85 已合并(c31c7e2),本仓 main 现在是 17.7.0。上一条评论里的探针在 17.7.0 上测得:那条错路(对 Field.datetime 求 avg 再相减)在每一道门都报错——作者时 os validate 拒绝、保存门 422、查询门 400。本卡自己的解锁判据成立了,「渲染出一个干净的错数」这个风险已经消失。

    但要知道:平台的修法是拒绝而不是提供(本卡 2026-09-09 的更正里已说明),所以 §09 原本想要的「各段时长 / 超 SLA」在 17.7.0 上仍然算不出来。现在的替代物(阶段覆盖数、审批吞吐、固定 30 天阈值且标题写明阈值)照 1C + 2B 继续有效。

    你要做的:回一个字母。

    做法 代价 / 收益
    A(推荐) 关单(完成):1C 的等待已经结束,危险的错数已被平台拒绝,替代物按 1C + 2B 保留 零工作量;§09 不再承诺算不出来的指标
    B 改选 1A:日任务把各段时长 / 超 SLA 盖成合同上的持久字段 §09 字面兑现;需要你定字段名与语义(公开契约),并动日任务与 §03

    四棱分析

    • 实际业务需求:周转时长与超 SLA 是法务负责人真实会问的问题;但目前没有任何用户或部署在等这两个数,拉动来自设计文档本身。
    • 项目长远合理性:A 停在平台能兑现的范围内;B 等于在应用里补平台不提供的度量——1C 当初的取向正是不在应用里绕路。
    • 防 AI 写错:平台现在会响亮拒绝错误写法,A 下不会再有 agent 造出错数;B 会引入新的盖戳字段与日任务,需要新的正确性保证。
    • 创业阶段不扩散:A 不扩散;B 新增字段与任务逻辑。

    Generated by Claude Code

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