Skip to content

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

Closed

Description

@hotlong

Ruled: 6396322580 · letter C · 2026-10-25T10:07Z (C = keep the goal, platform card for a datetime-difference measure; 1C + 2B of 2026-09-09 stand)
Blocked-by: objectstack-ai/objectstack#26601 (per-type over-SLA: the $now offset across contract_type; #25778 shipped in 18.2.0 and the durations landed as PR #168)

⬆️ 2026-10-25 第 3 批裁定 C:等待对象从 objectstack#16737(已关闭,修法是拒绝而非提供)换成 objectstack#25778(分析层提供「两个 datetime 列之差」的度量原语)。解锁判据不变:消费方可安装,且本仓能写出 §09 的那两个度量。
⬆️ 裁定 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

  8. 19 remaining items

  9. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    Unblocked — 2026-11-03

    objectstack#26601 closed: B′ (a to-one hop for the addDays $field offset) was declined (ruling 6457437597 on objectstack#26753). The director's pointer for this card (objectstack#26601, 2026-10-28T00:44Z) gives the platform-native answer under ruling 6396322580: a stored expression field on clm_contract (review_sla_days, read through contract_type), with the over-SLA dashboard filter on that column. The dev verifies the one-hop stored expression on one record first. main is on @objectstack/* 18.3.0 since PR #198 (fc68eb6, push CI green). State: pm:blocked → pm:queue.


    Generated by Claude Code

  10. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 18
    Ruling-ref: 6396322580
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-31-review-sla
    Worktree: hotclm-issue-31
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/objects/contract.object.ts (stored expression review_sla_days) · src/datasets/** · src/dashboards/** · src/translations/{en,zh-CN}/ (the new field and dashboard strings) — stop on breach; explain in the report
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: this app's legal dashboard (over-SLA reviews per contract type) | objectstack-ai/objectstack#26601 closed: B′ declined, platform-native answer is a stored expression field | the legal head
    Thread-read: none
    Premise: re-measured by the dev on origin/main fc68eb6 (@objectstack 18.3.0) with the declared demo staff
    Serial constraints cleared: 18.3.0 landed (PR #198). Dispatched together on disjoint files: #195 (lifecycle actions), #197 (demoStaff, README, demo script), #157 (child-name mirror), #31 (SLA field, datasets, dashboards); #195 and #31 both add translation keys to objects.contract.ts and rebase before their PRs.


    Generated by Claude Code

  11. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    Written 2026-11-03 · PR #201 · head 4e58df8 · branch claude/issue-31-review-sla

    {
      "issue": 31,
      "status": "done",
      "branch": "claude/issue-31-review-sla",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/201",
      "head": "4e58df8",
      "session": "session_017nzWFRUo2U7evMkPdVniAn (parent seat's; this run is an os-dev subagent)",
      "premise_still_valid": true,
      "summary": "The per-type over-SLA half of §09 is delivered on 18.3.0, and the PR body opens with 'Fixes #31' because the durations half landed in PR #168. Following the director's pointer (objectstack#26601, 2026-10-28T00:44Z), clm_contract gets a stored expression field read one hop through contract_type. It was first verified on one record with the pointer's exact spelling (positive review_sla_days): insert stored 3, re-picking the type stored 10, and a sent 99 did not stand. ONE DEVIATION, measured: 18.3's $now anchor ADDS its addDays column and has no subDays or negation, so 'now minus SLA' needs a column holding the negative number (the platform's own documented example names it review_sla_days_offset). The field is therefore review_sla_days_offset = 0 - record.contract_type.review_sla_days, readonly, hidden, in the identity group. The name is raised as open question 1. contract_metrics gains review_over_sla_count (count, status in_review AND review_started_at < {$now, addDays: {$field: review_sla_days_offset}}). On the legal workbench the fixed 30-day tile becomes 审查超时限, and a new per-type horizontal bar is added. No hook, job or flow writes the column.",
      "tests": "Throwaway DB on port 3167, pnpm demo:zh, @objectstack 18.3.0, every reading signed in as legal.head@example.com (password admin123, demo staff, no hand-made account). BEFORE (origin/main fc68eb6, fresh DB, Chromium zh-CN): 待受理 6 · 审查中 12 · 审查超过 30 天 6 · 等待对方 2. AFTER (fresh DB): one record as requester1, final field: POST NDA gives offset -3; PATCH contract_type to Lease gives -10; PATCH offset 5 returns 200 and the value stays -10. Edit of the type's own SLA is NOT MEASURED: clm.admin@ got PATCH clm_contract_type 403, and no other route was taken. Independent SQLite (julianday(review_started_at) < julianday('now') - type.review_sla_days), API (POST /api/v1/analytics/dataset/query as legal.head) and the rendered dashboard agree. Tile: 12 = 12 = 12. Per type: MSA 6, NDA 2, SUP 1, DPA 1, LSE 1, ICA 1, identical in all three. Under SLA: as legal.head, MSA-2026-0016 was moved submitted → in_review (legal_owner counsel1, offset -5, started now). 审查中 went 12 → 13 while 审查超时限 stayed 12, and the MSA bar stayed 6 of 7 in review. Over SLA: the DPA contract (17.7 days in review, SLA 5) is counted, and it was NOT in the old 30-day tile. runtimeFilter category=nda gives 2 = SQL 2. Selected alone, the measure's filter narrows the rows, so a type with none over SLA is not drawn (rows: [] measured). The chart description says so. The record page for MSA-2026-0016 does not show the hidden field. Second pnpm demo:zh run on the same DB: Seeds 1130 rows; no degraded, no-such-table or failed plugin; 0 of 121 offsets NULL; 0 disagree with -type SLA. Console before and after: the same one 404 resource and one pre-login 401 get-session. The browser readings were on fc68eb6 plus the change. The rebase onto 56ebd41 brought #199 and #200, which touch no dataset, dashboard or this field.",
      "gates": "Head 4e58df8 (rebased on origin/main 56ebd41), each exit captured before any pipe: pnpm install EXIT=0; pnpm validate EXIT=0 (✓ Validation passed); pnpm lint EXIT=0 (18 warnings = 17 security-superuser-field-mask-inert #196 + 1 obligation_requester_own #64, 6 suggestions: identical to main, none on a changed key); pnpm typecheck EXIT=0; pnpm lint:i18n-gate EXIT=0 (COVERAGE: 0 missing keys across 2 locales); CLM_DEMO_SEED=1 pnpm validate EXIT=0. Measured while placing the field: in the 'lifecycle' group it added a 19th warning, because CONTRACT_STAMPED_FIELDS derives a clm_admin FLS mask from that group. The field therefore sits in 'identity', and a stored expression needs no lock. CI check-runs on PR #201: not read (the report is delivered at the end of local verification; CI convergence is the seat's).",
      "dogfood_battery": "n/a — app repo, no `!` title, Clause-② no",
      "mcp_calls": "0",
      "api_writes": "2 — fleet-write relay: pr_create objectstack-ai/hotclm#201 (read-back identical, 8868 bytes, assignee zhuangjianguo); this os-dev-report comment on hotclm#31 via post-stamped. git push x1 (not REST).",
      "open_questions": [
        {
          "question": "The pointer spelled the field review_sla_days (positive copy of the type's SLA). 18.3's $now anchor can only ADD a same-record column, so the PR ships review_sla_days_offset (negative). Keep that name?",
          "options": [
            "A: keep review_sla_days_offset, one hidden column holding -SLA (as shipped; the platform's own documented spelling for this count in filter.zod.ts).",
            "B: two columns. A visible positive review_sla_days (the pointer's spelling, shown on the contract) plus the hidden review_sla_days_offset that the filter reads. One more stored column and two translation keys.",
            "C: name the column review_sla_days but store the negative value. ⛔ Listed only to reject: a column named for the SLA that holds -5 misleads every later reader."
          ],
          "recommendation": "A, because it is one column, its sign is forced by the platform and named the way the platform documents it, and nothing in the app needs a positive copy (the type already shows it; the cycle-time dataset reads it through the join). Choose B only if legal should see the SLA on the contract page."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: boot of this change over a DB created by origin/main (pnpm demo:zh, 18.3.0) left clm_contract.review_sla_days_offset NULL on 121/121 rows. A later demo seed pass rewrote only 41/120 rows (79 left NULL). The $now anchor reads a NULL offset as 0 days, so every started review counts as over SLA until its contract is next written. Adding a stored expression field to an object with rows has no backfill: the engine computes it only on a write, and `os migrate` has summary-nulls for roll-ups but nothing for stored expressions. Seam: spec:Field.expression (stored, field.zod.ts) → runtime: objectql computeStoredExpressions (write path only); migrate: no stored-expression backfill · dedupe words: \"stored expression backfill existing rows\", \"expression field added NULL existing records\", \"migrate stored computed field\", \"recompute stored expression on schema change\"",
        "class: none (platform contract, noted) · a stored one-hop expression does not follow an edit of the related record: changing clm_contract_type.review_sla_days leaves every filed contract's offset as it was until that contract is written. Not measured live (clm.admin@ was refused 403 on PATCH clm_contract_type), and stated from the field.zod.ts contract · carrier: none · noted in the PR acceptance notes, not filed",
        "carrier: none · noted, not filed: src/flows/legal-review-sla.flow.ts (F3 reminder) still fires at a fixed 30 days. The help text on clm_contract_type.review_sla_days says so and is still accurate. Both are outside this claim's surface. The reminder could now read review_sla_days_offset, so that the tile and the reminder tell one story again."
      ],
      "deviations": [
        "Field name review_sla_days_offset (negative), not review_sla_days, as measured above; see open question 1. The file is the claim's src/objects/contract.object.ts, so the surface is not breached.",
        "Browser and API readings were taken on fc68eb6 plus the change. The gates ran on the rebased head 4e58df8."
      ],
      "governed_text_proposal": {
        "DESIGN.md §03 字段清单, clm_contract row — insert after 'direction(同上)'": "· review_sla_days_offset number(合同类型 `review_sla_days` 的负数:平台存储表达式 `0 - record.contract_type.review_sla_days`,每次写入由引擎计算,客户端传值不生效;只读、隐藏;只供 §09「审查超时限」计数读取)",
        "DESIGN.md §09 法务工作台 bullet (on top of PR #168's proposal)": "- **法务工作台** — 待受理数 · 审查中 · 审查超时限(各合同按自己类型的 `review_sla_days` 判定)· 谈判停滞 · 平均审批周转(提交 → 批准的平均天数,本月对比紧挨在前的等长时段,用平台 `compareTo: previousPeriod`)· 各阶段合同数(横向条形图,按合同数从多到少排;只画在办的六个阶段)· 各段时长(四块砖,每段一块,单位天)· 审查超时限按合同类型(横向条形图,按超时限数从多到少排;没有超时限合同的类型不画)",
        "DESIGN.md §09 paragraph on 各段时长 / 超 SLA (replaces the 2B paragraph and PR #168's proposed replacement)": "**各段时长与按类型的「超 SLA」均已交付**(维护者裁定 2026-09-09 1C + 2B,2026-10-25 C:保留目标、等平台)。各段时长建在平台 18.2.0 的 `duration` 度量上(`objectstack-ai/objectstack#25778`)。「审查超时限」拿 `review_started_at` 比「现在减去该合同类型的审查时限」:平台 `$now` 锚点(`objectstack-ai/objectstack#25808`,裁定 A)的偏移只读同一条记录上的数值列,经 `contract_type` 的一跳路径不被采纳(`#26601`、`#26753` 拒绝 B′)。因此合同上存一列 `review_sla_days_offset`,是平台存储表达式(18.3.0),由引擎在每次写入时从合同类型算出。`addDays` 只做加法,所以存的是负数。⛔ 不得用应用侧 hook、日任务或流程盖戳字段来补时长或超时限 —— 那是在应用里复刻平台规则;`review_sla_days_offset` 由平台计算,不在此列。〔「审批瓶颈」一段原样保留〕",
        "DESIGN.md §02 table, Insights row": "| Insights | 周转分析 | 阶段时间戳 → dataset(平台 `duration` 度量)→ 看板:各段平均时长、平均审批周转、按类型的审查超时限(§09) |",
        "DESIGN.md §12": "Do NOT add the row PR #168 proposed ('按类型超 SLA … 跨对象偏移待新卡'). It is resolved by this PR. If the seat's PR #120 already carries PR #168's ⛔ sentence 「也不得在合同上镜像一列 SLA 天数」, that sentence must give way to the paragraph above, because it would forbid this PR's platform-computed column."
      }
    }

    Generated by Claude Code

  12. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    Contributor

    Ruling-ref: 6396322580

    Landed — PR #201 · 2026-11-03

    reading value
    PR #201 MERGED (squash on main), merged at its ACCEPT head 4e58df8; Fixes #31 closes this card
    authority maintainer, project thread, verbatim: 「你可以自己审核的就自己合并」

    Release: the legal workbench counts reviews over their own type's SLA (审查超时限, plus a per-type bar), replacing the fixed 30-day tile. The per-stage durations landed earlier as PR #168, so both halves of §09 are now delivered. The governed §02/§03/§09/§12 text is on PR #120, which the maintainer merges. Follow-ups: the F3 reminder still uses 30 days (new hotclm card), and an upgraded database is not backfilled (new objectstack card). State: pm:dispatched removed, assignee cleared.


    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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions