Repository navigation
[Decision] An analytics filter cannot read SLA days off the contract type: amend ruling A on #25808 so the addDays $field offset follows one to-one hop (B′), or keep A (hotclm copies review_sla_days)? #26753
Description
Ruled: 6457437597 · letter A (B′ declined; closes not_planned) · 2026-10-28T00:38Z
Filed by domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · drafted on claude-fable-5-1 · 2026-10-27
⛔ Not a claim. ⛔ Not a dispatch. This card asks for one letter; nothing is built until it is answered.
Filing gate: ② a decision only the maintainer can make — amending ruling 6415710193 (letter A on #25808, 2026-10-26T12:12Z). The seat ruling on #26601 (6439436943) declined to reopen B′ because a seat cannot amend a maintainer ruling; hotclm's maintainer ruled (hotclm#31, 6396322580, letter C) to wait for the platform rather than stamp durations app-side, so a "no" here is also an answer that goes back to that maintainer. Every reading below is on origin/main 1685a06d (fetched 2026-10-27).
Governing text:
packages/spec/src/data/filter.zod.ts:86-90(the 2026-08-06 same-table rule): "The maintainer's 2026-08-06 rulings bound what compiles: same-table columns only (a dotted path is refused — no JOIN planning, no alias contract), declared columns only, the tenant-isolation column on neither side, and the same comparison class on both sides. Every position outside that boundary is refused withINVALID_FILTER(HTTP 400), never bound as a literal."packages/services/service-analytics/src/strategies/native-sql-strategy.ts:666-672(the 2026-08-12 rule, ruling record5261751132on [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598, Q1 = B, quoted as written): 「NativeSQLStrategy.canHandle对携带$field的where/ read scope decline,落回 ObjectQL/engine 路径,由 driver 用它自有的 metadata 强制全部四条 [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 裁定 —— 安全规则只存在一处,不复制、不新增StrategyContext钩子、不动packages/spec。」- Ruling
5548479553on [finding] The ruling's own offset shapecompleted_at <= due_date + duty.grace_daysis refused on SQL push-down — a cross-field reference (and itsaddDaysoffset) may not name a relation column, so thedulydataset needsgrace_dayson the task or a join-aware push-down ruling #15103 (2026-09-05): widening toincluded relations was ruled A gated on one measurement, with fallback B ruled in advance ("capability unchanged … the boundary named"); the measurement answered B and PR docs(objectql): name the include-relation boundary of a cross-field comparand, and pin it (#15103, fallback B) #15781 (+256/−0, 2 files) pinned it —packages/services/service-analytics/src/__tests__/include-relation-cross-field-boundary.test.ts:1-50: "the dotted comparand is refusedINVALID_FILTER/ 400 EVEN WHEN the relation is ininclude". packages/spec/src/data/filter.zod.ts:189-193(ruling A recorded in the spec): "Both standing rulings hold unchanged: the offset column is a column of the SAME record (letter A; a path-valued offset — B′ — was not taken), and no JOIN is planned for it. [Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601] So a dotted offset is refused at the authoring doors too."- ADR-0021 D-C (
docs/adr/0021-analytics-dataset-semantic-layer.md:118,183,383): "Zero raw SQL / zero raw expressions"; joins only along declared relationships, derived from the object graph. ADR-0071 (docs/adr/0071-dataset-semantic-layer-depth.md, Decision):Dataset.includetakes to-one paths ≤3 hops and the compiler emitsLEFT JOINs for dimensions and measures — a filter comparand is not named there. .claude/skills/pm-dispatch/references/decision-analysis.md〈应用仓推荐序特例〉: capability is built on the platform first; an app repo does not build a substitute, and a workaround is never the recommendation.
Ruling-ref: 6415710193
Related: #25808 (parent, ruling A) · #26601 (closed via PR #26655, seat ruling 6439436943) · #15103 (ruling 5548479553, PR #15781) · #7598 (ruling 5261751132) · #14104 / PR #15102 (addDays) · PR #26261 (the $now anchor) · hotclm#31 (6396322580 C) · hotclm PR #168
Reader who acts: the maintainer rules. On A nothing moves in this repository; hotclm is told the platform answer. On B′ this card becomes the execution card (domain:spec, with driver-sql and formula hunks), and the three standing texts above get a new ruling line.
维护者速读
- 一句话问题: 合同审查看板要数「审查开始已超过 SLA 天数」的合同,而每种合同类型的 SLA 天数不一样(hotclm 九种类型,2、3、5、10 天)。今天过滤条件可以写「现在减去本记录某一列的天数」,但不能写「现在减去关联记录(合同类型)某一列的天数」:
addDays: { $field: 'contract_type.review_sla_days' }在保存、os validate和读取时都被响亮拒绝(fix(spec)!: the filter save and validate doors refuse a dotted addDays offset #26655 已落地)。 - 谁撞上: hotclm 的法务负责人(hotclm PR 🔗 Broken links detected in documentation #168、hotclm#31)。上次(Analytics datasets: no filter compares a datetime column against now minus an SLA held on a related record #25808)你裁了 A:偏移量只认同一条记录的数值列,hotclm 把
review_sla_days复制到合同上。hotclm 这边又问了一次,所以这张卡把 B′ 正式呈你一次;席位无权改你的裁决。 - 选项:
| 选项 | 做什么 | 用户看到 | 工作量 |
|---|---|---|---|
| A 维持(推荐) | 平台不动。hotclm 在 clm_contract 上声明一个数值字段 review_sla_days,expression: record.contract_type.review_sla_days(平台在每次写入时沿一跳自动算、自动存,不用写 hook);看板过滤写 review_started_at < { $now, addDays: { $field: 'review_sla_days' } } |
看板正常计数;合同类型的 SLA 改了,已有合同要等下次写入才跟上(按「审查开始时生效的 SLA」理解) | 平台 0(#26261 +2587/−95、35 文件,#26655 +414/−39、10 文件,均已合并)。hotclm S:一个字段 + 一个度量,本会话无 hotclm 仓访问权,未实测 |
| B′ 放开一跳 | 偏移量允许写 a.b(一跳 to-one 关联,lookup / master_detail),driver-sql 为这个偏移量编一个 LEFT JOIN;数据过滤、数据集过滤、度量过滤同一条规则 |
看板直接读类型上的 SLA,类型改了立即生效 | domain:spec + driver-sql + formula 一个 PR、合同审查档;按 6414128401 读数 ≈ A 再加 300–500 行源码、1.5–2 倍、4–5 dev-day;跨表 JOIN 在两条路径上都无落地先例(#15103 实测后选了回退 B,PR #15781 只钉了边界);并且推翻 2026-08-06「不做 JOIN」与 2026-09-05 #15103 回退两条既裁 |
- 业务含义: A = Salesforce 的做法:合同上放一个跨对象公式字段
Contract_Type__r.Review_SLA_Days__c,报表过滤读合同自己的字段;SOQL 的 WHERE 本来就不能「日期 < 今天 − 父记录的天数」。B′ = 让过滤条件自己去 JOIN 父表取参数,主流平台没有这个形状。 - 推荐 A。 只看长远:三条既裁(08-06、08-12、09-05)都把跨列比较钉在「同表、一处执行」,B′ 是第四次问同一个问题,而且要在引擎路径里新开一个 JOIN 契约;A 的形状平台今天已经能声明(存储表达式一跳),不需要应用写 hook。盲区:存储表达式沿一跳取值这条在引擎 docblock 里写明、未找到逐行测试钉子(见前提 6)。
- 请回:
A / B′。
选项细节
A — keep ruling A; the author-time refusal (#26655) stays. The platform contract is as filter.zod.ts:189-193 states it. hotclm's half, in platform-native form: a stored computed field on clm_contract — review_sla_days: { type: 'number', expression: record.contract_type.review_sla_days }. Per packages/objectql/src/engine.ts:3028-3040 (planStoredExpressions, #24619) a non-formula field with an expression has a column, is computed by the engine "on every write — the declared expression over the row the write stores", and is planned "with the formula read plan's own entry builder … so the scale rounding and the one hop are the read's"; both write paths wire relatedFor: resolveFormulaRelated (engine.ts:14912, :16324-16330). So the copy ruling 6415710193 item 6 asks for needs no hook: it is a declared field, recomputed on insert and on every update by id, and on a predicate update only when the patch moves contract_type (field.zod.ts:1869). The one business caveat: a change to clm_contract_type.review_sla_days reaches an existing contract on its next write, not immediately — which reads as "the SLA in force when the review started", a defensible SLA semantic, but it is a semantic and the card says so. The measure filter is then review_started_at: { $lt: { $now: true, addDays: { $field: 'review_sla_days' } } } with the column holding negative days, the spelling content/docs/protocol/objectql/query-syntax.mdx:545-547 teaches.
B′ — compile a to-one path offset. Strategy (ii) of reading 6414128401: the engine path (driver-sql) learns a one-hop LEFT JOIN for the offset operand only — applyCrossFieldComparison (sql-driver-cross-field-comparison.ts:350-354) and applyNowAnchorComparison (:515-519) resolve a.b through the declared lookup / master_detail field a's reference, join that table on its primary key, and bind COALESCE("a"."b", 0) into crossFieldOffsetExpr. Enforcement stays in one place (the driver), so the 2026-08-12 rule's reason survives; what it reverses is the 2026-08-06 sentence "no JOIN planning, no alias contract" and the 2026-09-05 #15103 fallback ("capability unchanged … a dataset's include does not widen it", query-syntax.mdx:589-595). Strategy (i) — a cross-field compile branch inside NativeSQLStrategy riding ADR-0071's qualifyAndRegisterJoin — is not proposed: it is option A of the 2026-08-12 ruling, rejected there by name ("为无实测消费者的钩子建第二套枚举面"), and it would leave ordinary data filters refusing what dataset filters accept. Under B′ the spec (filter.zod.ts:295-300 AddDaysReferenceSchema regex, filter-comparand-type.ts:295-311 dottedAddDaysOffset / dottedAddDaysOffsetRefusal) narrows its refusal to two-or-more hops and to a first segment that is not a declared to-one field; the memory evaluator (@objectstack/formula matches-filter.ts) must either expand the hop or refuse, because today it reads a lookup's id on a stored row and counts zero days (PR #26655 body, "Executors measured"); driver-mongodb keeps refusing; cross-field-conformance-cases.ts gains the relation cells on both paths; validate-filter-tokens and the docs follow. Workload: one PR at CONTRACT_REVIEW_TIER, Clause-②: yes (widening), Metadata-protocol: added; per 6414128401 ≈ A + 300–500 source lines, 1.5–2× A, 4–5 dev-days; measured unit PR #26261 (+2587/−95, 35 files, merged 2026-10-26T19:11Z). No landed precedent for a join in either cross-field path — 「无先例」; the nearest measurement is #15103 → PR #15781, which measured the join chain and answered that it does not reach the where compilation.
Why there is no third letter. A declared formula field is virtual and cannot be the offset: packages/spec/src/data/filter-cross-field-comparison-class.ts:145 lists formula in CROSS_FIELD_NO_CLASS_REASONS ("virtual: no stored column"). A summary roll-up aggregates children, and clm_contract_type is the parent. The stored-expression field above is the only platform-native route, and it is A's execution, not a new contract — so it is written under A rather than as C. The documented "stamp a due date" route is hotclm#31's letter C, declined there; not re-offered.
Premises, each with its re-check
- The dotted offset is refused at save and
os validate—git show origin/main:packages/spec/src/data/filter.zod.ts | sed -n 295,300p(regex/^[^.]*$/onAddDaysReferenceSchema.$field);git grep -n 'dottedAddDaysOffset' origin/main -- packages/spec/src/data/filter-comparand-type.ts→:295,:415. Holds. - Every SQL read refuses it —
git show origin/main:packages/drivers/driver-sql/src/sql-driver-cross-field-comparison.ts | sed -n 350,354p(pair offset),| sed -n 515,519p(now-anchor offset),| sed -n 268,272p(bare reference). Holds. (Seat ruling6439436943cited:270; that is the bare arm; the offset arms are:350and:515on this head.) - The native strategy declines every cross-field comparand so the engine path compiles it —
git grep -n 'findCrossFieldComparand' origin/main -- packages/services/service-analytics/src/strategies/native-sql-strategy.ts→:38,:1063,:1084; ruling quoted at:666-672. Holds. - The engine path has no join to ride —
git show origin/main:packages/services/service-analytics/src/strategies/objectql-strategy.ts | sed -n 1142,1162p("the engine cannot join in an aggregate");executeAggregatecarries noinclude(include-relation-cross-field-boundary.test.ts:28-33). Holds. Dataset.includeis to-one paths ≤3 hops —git show origin/main:packages/spec/src/ui/dataset.zod.ts | sed -n 595,606p. Holds.- A stored expression on a non-
formulafield reads one hop at write time —git show origin/main:packages/objectql/src/engine.ts | sed -n 3028,3040p;relatedForwired at:14912(insert) and:16324-16330(update). Docblock-stated;engine-stored-expression-write.test.tscarries no one-hop case (git grep -n -i 'hop' origin/main -- packages/objectql/src/engine-stored-expression-write.test.ts→ 0 hits) → the row-level behaviour is 「未验证」; a dev on A's hotclm side verifies it with one record before relying on it. - hotclm's ask —
objectstack-ai/hotclmis not attached to this session (REST 403), so the ask is cited as Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601 quotes it: nine contract types,review_sla_days∈ {2, 3, 5, 10}, measured on @objectstack 18.2.0 from hotclm PR 🔗 Broken links detected in documentation #168 as the head of legal; hotclm#31 ruling6396322580C. 「未验证」 against hotclm itself.
The four axes
- Long-term fit (≥50%). Two years out, the platform's filter grammar should still be one closed rule an author can hold in one sentence: a comparand is a literal, a same-record column, or now, shifted by whole days held on the record. Salesforce reaches a parent's parameter through a cross-object formula field on the record, never through a filter that plans its own join, and SOQL's WHERE has no column arithmetic at all; Looker and Cube put a derived value in the model, not in the predicate. ObjectStack already has that shape — a stored computed field reading one hop (18.0 round-2 sweep framework findings: a paid invoice line's
amount/totalstays writable while its inputs are locked; an unevaluable hook condition answers 500 INTERNAL_ERROR;remote-tables?schema=never narrows on the SQL drivers #24619 / A formula field that reads through a reference (record.book.title) passes validateExpression, then reads null on every row: the read path projects only the row's own columns #24197) — so A is the end-state, not a stopgap. B′ is the fourth time the same boundary is asked to move (2026-08-06, 2026-08-12, 2026-09-05, 2026-10-26), and each move so far was declined after measurement; taking it now opens a join contract inside the engine path that every later$fieldask will cite. → A. - Business pull. One consumer, hotclm, one column, measured only through Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601's quotes (premise 7). The maintainer's own ruling item 6 already gave hotclm its route; what B′ buys hotclm is "edit the SLA on the type and existing contracts follow at once" — a real but narrow gain, and arguably the wrong SLA semantic for reviews already underway. The pull does not clear the ≥3× workload gap the reading measured. → A, with the honest note that the 2026-09-05 facet ④ called "pushing a copy hop into every affected app" the expansion to refuse — the stored-expression route answers that objection without a hook or a second writer.
- Guarding against AI-authored mistakes. Today a dotted offset is refused loudly at save, validate and read, in one sentence (fix(spec)!: the filter save and validate doors refuse a dotted addDays offset #26655). Under B′ the same spelling becomes legal for one hop and stays refused for two, and the memory evaluator must learn to expand the hop or an AI-authored filter answers silently wrong (zero days) on a stored row — the exact fault PR fix(spec)!: the filter save and validate doors refuse a dotted addDays offset #26655 measured. A keeps the closed rule; B′ adds a conditional one. → A.
- Startup-stage non-proliferation. A adds no platform surface. B′ adds a permanent join obligation in
driver-sql(or, under (i), a second cross-field compiler) plus the dual-path conformance it owes forever, for one app's one column. → A.
Self-check: 只看①选 A;②③④ 是否翻转:否。
os-decision-facets
- ① 项目长远合理性:A 缩小特例——比较值规则仍是一句话(字面量、同记录列、或「现在」加本记录持有的天数),与 Salesforce 跨对象公式字段 + 报表过滤只读本记录的建模一致;B′ 在引擎路径新开一条 JOIN 契约,是同一边界第四次被要求移动。
- ② 实际业务拉动:今天撞上的只有 hotclm 一个应用、一列(九种类型,2/3/5/10 天,引自 Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601,本会话无 hotclm 仓访问);裁决 6415710193 第 6 项已给出路,B′ 多买到的只是「类型上改 SLA 立即生效」。
- ③ 防 AI 犯错:A 下带点偏移在保存、validate、读取三处同一句响亮拒绝(fix(spec)!: the filter save and validate doors refuse a dotted addDays offset #26655);B′ 让一跳合法、两跳仍拒,且内存求值器若不展开关联就按 0 天静默算错——正是 fix(spec)!: the filter save and validate doors refuse a dotted addDays offset #26655 实测到的故障。
- ④ 创业阶段不扩散:A 零新增平台面;B′ 给 driver-sql 加一条永久的 JOIN 义务(或原生策略第二套跨列编译)和双路径一致性测试,为一个应用的一列买单。
Prior rulings read: addDays dotted / $field JOIN same-table / $now anchor → 5 rulings read by id (6415710193, 6414128401, 5548479553, 5261751132, 6439436943) + #26601's dedupe (#25808, #26301); ADR-0021 D-C, ADR-0071; thread: #26601 6439436943 · full-text issue search unavailable in this session (search/issues 403, repo-bound) → unresolved.
裁后执行
- A: close this card
not_plannedciting this ruling; comment on Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601 that B′ was put to the maintainer and declined, so hotclm's answer is ruling6415710193item 6 in the stored-expression form above (a seat relays it to hotclm#31 / hotclm PR 🔗 Broken links detected in documentation #168; the hotclm dev verifies premise 6 on one record first). NoobjectstackPR. Optional one-line follow-up for the skills lane:skills/objectstack-query/rules/filters.mdnames the stored-expression field as the way to hold a related value (governed surface, its own PR). - B′: this card becomes the execution card —
domain:spec, p2 (app-driven addition),area:reports; a new ruling line amends the 2026-08-06 sentence (one declared to-one hop for the offset operand, driver-planned, offset only) and the 2026-09-05 [finding] The ruling's own offset shapecompleted_at <= due_date + duty.grace_daysis refused on SQL push-down — a cross-field reference (and itsaddDaysoffset) may not name a relation column, so thedulydataset needsgrace_dayson the task or a join-aware push-down ruling #15103 fallback; the seat dispatches one card thread at CONTRACT_REVIEW_TIER withRuling-ref:naming the new comment; the dev verifies premises 1–4 first and reports a fork if any fails; ⛔ no second cross-field compiler inNativeSQLStrategy, ⛔ no two-hop, ⛔ no base-$fieldpath (the bare{ $lte: { $field: 'duty.due_on' } }asymmetry stays its own question). The three governing texts above are rewritten in the same PR;@objectstack/specand@objectstack/driver-sqlminor.
Duplicate search. objectstack issues read this round: #25808 (closed, ruling A), #26601 (closed, PR #26655), #15103 (closed, PR #15781), #7598 (closed, PR #7916), #14104 (closed, PR #15102). No open card asks for a path-valued addDays offset. Full-text search was not available to this session (see Prior rulings read).
Activity
objectstack-fleet commentedon Oct 27, 2026
Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #415 · 2026-10-27T22:54Z
Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #415 as item 1 of 5 (five spec/server/ui decisions). Seat increment (re-verified on
origin/mainaa00362fe5; the card is not restated):filter.zod.ts:86–90(same-table only, dotted path refused, no JOIN) and:189–193(ruling A recorded: the offset is a column of the SAME record, B′ not taken, [Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601] refused at the authoring doors too); ruling 6415710193 item 6 gives hotclm its route ("keepsreview_sla_daysonclm_contract(copied from the type on create / type change)").planStoredExpressionsexists atengine.ts:3069; the card's premise 6 (a stored expression reads one hop at write time) is docblock-stated, not test-pinned — the hotclm dev verifies it on one record before relying on it, as the card says.needs-user-decisionalone; the ruling's stroke closes the card (A) or queues it (B′).review_sla_days: { type: 'number', expression: record.contract_type.review_sla_days }and filters on it). Not B′. Workload: platform 0; hotclm one field declaration.Prior rulings read: 6415710193 (A), 6414128401, 5548479553 ([finding] The ruling's own offset shape
completed_at <= due_date + duty.grace_daysis refused on SQL push-down — a cross-field reference (and itsaddDaysoffset) may not name a relation column, so thedulydataset needsgrace_dayson the task or a join-aware push-down ruling #15103 → B), 5261751132 ([spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 Q1 B), 6439436943; ADR-0021 D-C, ADR-0071; thread: Analytics: the $now anchor's addDays offset cannot read a related record's column (SLA per contract type) — and os validate passes the dotted path the runtime refuses #26601.objectstack-fleet commentedon Oct 28, 2026
Ruling: batch #415 item 1 · letter A · maintainer 「同意」 2026-10-28T00:34Z
Recorded by the director seat (summon #51 continuation,
session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708). The maintainer answered batch #415 with 「同意」 — every seat letter. Brief 6455600109. Letter A — ruling 6415710193 (A on #25808) stands; B′ is declined a second time: theaddDays$fieldoffset is a column of the same record; no to-one hop, no driver-planned JOIN for the offset operand; the authoring-door refusal of #26655 stays. hotclm's platform-native route is a stored expression field onclm_contract—review_sla_days: { type: 'number', expression: record.contract_type.review_sla_days }(computed by the engine on every write,planStoredExpressions,engine.ts:3069) — and the dashboard filter reads that column:review_started_at < { $now, addDays: { $field: 'review_sla_days' } }. The hotclm dev verifies the one-hop stored expression on one record first (the card's premise 6 is docblock-stated, not test-pinned); if it does not read the hop, that is a platform defect to file, not a reason for B′.Workload: platform 0; hotclm one field declaration (hotclm#31 / PR #168). The three standing texts (
filter.zod.ts:86–90,:189–193,native-sql-strategy.ts:666–672) gain no new line.Execution:
needs-user-decisionleaves and this card closesnot_plannedin this act; a one-line pointer goes on #26601 for the hotclm relay.Ruling-ref:names this comment.Clause-②: no,Metadata-protocol: none.objectstack-fleet commentedon Oct 28, 2026
Closed
not_plannedby the director seat · 2026-10-28T00:45ZRuled A by the maintainer (ruling 6457437597, 「同意」 to batch #415): ruling 6415710193 on #25808 stands and B′ (a to-one hop for the
addDaysoffset) is declined; hotclm's route is a stored expression field on the contract (pointer on #26601, 6457582961). Nothing to build in this repository.