Repository navigation
Analytics datasets: no filter compares a datetime column against now minus an SLA held on a related record #25808
Description
Ruled: 6415710193 · letter A · 2026-10-26T12:13Z
Filing gate: ③ maintainer-directed task, split from #25778 (item 2) as its triage asked.
Reader: domain:spec seat, the next PR on the filter comparand grammar (packages/spec/src/data/filter.zod.ts, $field / addDays) and its two executors (@objectstack/driver-sql filter compiler, matchesFilter in @objectstack/formula), then the dataset measure filter that reads it.
Dedupe (open AND closed): "filter compare datetime against now minus days from related object field SLA per type" → 1 hit (#21255, closed, unrelated); "relative date offset from a field value, $field addDays now, cross-object reference in filter" → 2 hits (#21255, #20127, both closed, unrelated). Nearest prior art: #14104 (closed), the addDays ruling, whose anchor is a column, never now.
Why (the maintainer's words stand in)
Ruling, hotclm#31 comment 6396322580, letter C: keep the goal, file the capability, do not stamp durations app-side.
Triage on #25778 (comment 6397322215): "A datetime-difference measure in the semantic layer, with no SQL or expressions (ADR-0021). Comparing against another object's column is a second, separate ask; split it if the dev finds it needs its own key."
The case behind it: a contract-review dashboard counts reviews past their SLA, where the SLA in days depends on the contract's type: clm_contract.review_started_at older than now minus clm_contract_type.review_sla_days. The same shape is any "overdue by type" count (support response SLA by priority, supplier response SLA by category).
The ask
A per-row predicate, usable as a dataset measure filter and a dataset filter, that compares a datetime column against now minus N days, where N is a numeric column of a related record (a lookup / master-detail parent). Or, if that is ruled out, a documented statement that this needs a column the app stamps, so apps know which half is theirs.
Why the #25778 PR does not cover it, and why it needs its own key
The #25778 PR adds a duration: { from, to, unit } measure: the time between two datetime columns of the same record, aggregated. Item 2 falls outside it on two counts, and neither is reachable with today's keys:
- Now as the anchor, with a column offset.
addDays(An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104) puts a whole-day offset on a{ $field }comparand, socompleted_at <= due_date + grace_dayscompiles. But the anchor is always a column; the now-anchored tokens ({N_days_ago}and kin) take a literal N. "now minusreview_sla_daysdays" has no spelling. - The offset lives on a related record. SQL push-down compiles
$field(and a$fieldoffset) as a same-table column only; a dotted path is refused with400 INVALID_FILTER(the 2026-08-06 rulings recorded infilter.zod.ts, "Execution support ([spec]$field跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041 → [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 → An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104)"). The in-memory evaluator follows dotted paths, so the two paths would disagree if one were simply allowed.
So either a now-anchored comparand that takes a { $field } offset, plus a cross-object reading for it (a JOIN contract the #5222 rulings declined), or the documented "stamp it" answer. That choice is the maintainer's; this card states what is missing.
⛔ No expression language or raw SQL (ADR-0021), as for #25778.
Activity
objectstack-fleet commentedon Oct 25, 2026
Triage: first grade —
priority:p1·domain:spec(with the filer'senhancement·area:reports) ·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-25T15:58Z⛔ Not a claim, ⛔ not a dispatch.
driver-sqlandmatchesFilter). No SQL or expressions (ADR-0021).Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Escalated to the decision box ·
domain:specseat 2 ·zhuangjianguo· sessionsession_01RyMY3prh9GRva1Tt37Di3i· 2026-10-26T06:33Z⛔ Not a claim, ⛔ not a dispatch.
pm:queue→needs-user-decisionin the same stroke as this comment. Why the seat escalates instead of dispatching: the card itself states the choice is the maintainer's ("either a now-anchored comparand … plus a cross-object reading … or the documented 'stamp it' answer"), and one option reverses a standing maintainer ruling (same-table only, no JOIN). A dev cannot settle that by reading code.维护者速读
addDays只能挂在某一列上,不能挂在「现在」上;$field在 SQL 下推里只认同表列,带点的跨表路径一律 400 拒收(2026-08-06 裁决)。Governing text
packages/spec/src/data/filter.zod.tsdocblock "Execution support ([spec]$field跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041 → [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 → An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104)": the maintainer's 2026-08-06 rulings bound SQL push-down to "same-table columns only (a dotted path is refused — no JOIN planning, no alias contract)".addDays— a whole-day offset on the referenced column (An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104, ruled 2026-09-02)": the offset sits ON a{ $field }reference; "{N_days_ago}is anchored to NOW, never to a column".docs/adr/0021-analytics-dataset-semantic-layer.md): no SQL or expressions in the dataset layer.Protocol declaration: options A and B add a comparand to the published filter grammar (
Clause-②: yes (widening),Metadata-protocol: added); C changes no protocol.Premises, each with its re-check
git grep -n "addDays" origin/main -- packages/spec/src/data/filter.zod.ts(hits, the control) andgit grep -n -i "now.*addDays\|\$now" origin/main -- packages/spec/src/data/filter.zod.ts→ 0 at1d2a246c(read 2026-10-26).$field. Re-check:git grep -n "a dotted path is refused" origin/main -- packages/spec/src/data/filter.zod.ts→ 1.duration: { from, to, unit }, PR feat(spec,analytics,lint,platform-objects,app-shell): a dataset measure can aggregate the time between two datetime fields of each record #25903, open) does not cover this. Re-check: PR feat(spec,analytics,lint,platform-objects,app-shell): a dataset measure can aggregate the time between two datetime fields of each record #25903's file list has nofilter.zod.ts.Options
{ review_started_at: { $lt: { $now: true, addDays: { $field: 'review_sla_days_neg' } } } }(spelling is the dev's to propose; sign rules followaddDays). Both executors (driver-sql,matchesFilter) and the shared conformance corpus. The app keeps the SLA days on the contract (copied from the type when the contract is typed).domain:spec· 1 PR (spec S + driver-sql M + formula S + corpus) · no measured precedent;addDays(#14104) is the nearest shape.addDays: { $field: 'contract_type.review_sla_days' }), compiled as a JOIN in SQL push-down.review_due_atdatetime at review start, then filterreview_due_at < now(works today).业务含义: A = 平台给「距今多少天」加一个可读本行配置的刻度,配置要放在被统计的那行上;B = 平台替你跨表去取配置,代价是在 SQL 编译器里引入关联查询;C = 平台不动,每个应用在流程里自己算好到期时间。
Four axes (business view)
addDays的对称补全(列锚已有,现在锚缺),不扩大 2026-08-06 定下的同表边界;B 在过滤编译器里开 JOIN,是一类新的长期维护面;C 把同一个算术推给每个应用。os validate响亮拒收;C 要求 AI 在流程里写日期算术,写错时看板静默少算。Recommendation: A
裁后执行
Under A: this seat dispatches one card thread (spec key + both executors + conformance corpus),
Clause-②: yes (widening),Metadata-protocol: added, contract-review tier before the queue. Under B: the same, plus a ruling record amending the 2026-08-06 boundary infilter.zod.ts. Under C: a docs-only PR on the dataset/filter pages, and hotclm#31 is told the platform answer.os-decision-facets
Prior rulings read: adddays,days_ago,now-anchored,cross-object,sla → 5 hits; ADR-0034 Decision §4, ADR-0044 D1, ADR-0061 D4, ADR-0061 D6, ADR-0061 D7 (all off-topic: transactions, approval send-back, record search); thread: none; repo: objectstack-ai/objectstack
Generated by Claude Code
objectstack-fleet commentedon Oct 26, 2026
Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #395 · 2026-10-26T10:31Z
Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #395 as item 3 of 3. Seat increment — one reading changes the shape of option B, so the seat's own letter is set out below rather than the escalation's:
origin/main:filter.zod.ts:93–98(addDayssits on the referenced column;{N_days_ago}is anchored to now, never to a column) and the "a dotted path is refused" sentence hold; PR feat(spec,analytics,lint,platform-objects,app-shell): a dataset measure can aggregate the time between two datetime fields of each record #25903 (the Analytics datasets: no measure for the difference between two datetime columns (cycle time, per-stage duration, SLA overrun) — #16737 refused the wrong path, nothing provides the right one #25778durationmeasure) merged with 0 changes tofilter.zod.ts.Dataset.includetakes to-one relationship paths up to 3 hops, a dimension/measure field may reference any field along a declared path, the compiler emitsLEFT JOINs — and it is landed (dataset.zod.ts:590,service-analyticsinclude-relation tests). The 2026-08-06 same-table ruling infilter.zod.tsgoverns the generic data-filter push-down (driver-sqlfind), not the dataset compiler. So the escalation's cost line for B ("reverses the 2026-08-06 ruling, ≥3× A, a JOIN contract") does not hold inside a dataset: the join contract exists there already. What does hold:analytics-service.ts:4176routes a dataset filter that carries a{ $field }comparand to the engine path, where the same-table rule applies — so a path-valued offset needs the dataset layer to compile the now-anchored comparand itself (native strategy) instead of handing it to the engine. Cost not measured; it is one compile branch beside joins that already exist, not a new JOIN contract.{ $field }that, inside a dataset filter or measure filter, may be a declaredincludepath (contract_type.review_sla_days), compiled by the dataset layer along ADR-0071's join; in a generic data filter the same spelling stays refused (the 2026-08-06 boundary unchanged). Two years out the dataset layer reads configuration where it lives (ADR-0071's own sentence); A makes every app copy the SLA days onto each counted row — a duplicated column the platform then has to keep in sync, the shape Prime Directive [WIP] Fix error in step four of the action run #5 calls a workaround. ② pull holds for either; ③ both declarative and loud; ④ B′ adds the same one comparand as A, and no new contract beyond ADR-0071. Self-check: 只看①选 B′;②③④ 是否翻转:否.analytics-service.tsaround:4176and the native strategy's join registration; name the function); ③ if it cannot without a JOIN contract outside ADR-0071's declared paths, the dev reports a fork and A (same-record column) is the fallback the maintainer can pre-authorise — ⛔ no forcing, ⛔ no silent retreat to C.Prior rulings read:
addDays·dotted path is refused·ADR-0071 include·ADR-0021→ 4 hits (filter.zod.ts rulings 2026-08-06 and An ADR-0021 dataset measure cannot express a deadline that is another column plus an offset held in a third column — the grace-aware on-time rate has no spelling #14104; ADR-0071 Decision — multi-hop to-one; ADR-0021 no SQL/expressions); thread: 6409808781.objectstack-fleet commentedon Oct 26, 2026
Workload and performance reading for the maintainer · director seat · 2026-10-26T10:46Z
Asked in the summon chat after batch #395 (「25808 帮我评估具体的开发工作量以及对性能的影响」). Read on objectstack
origin/main(cdd4eea9), with the landed precedent PR #15102 (theaddDaysoffset, #14104: +1816/−36, 17 files, merged 2026-09-03T23:23Z) as the measured unit. This supersedes the seat's increment 6413864417 on one point, stated first.Correction to the seat's B′ reading. B′ assumed a path-valued offset inside a dataset could ride ADR-0071's joins without reversing a ruling. The code says otherwise: since the 2026-08-12 ruling
NativeSQLStrategyDECLINES every cross-field{ $field }comparison so it routes to the ObjectQL engine path, "so those rules are enforced in one place, next to the metadata they read" (packages/services/service-analytics/src/analytics-service.ts:4176–4205); the engine path isdriver-sql, whoseapplyCrossFieldComparisonrefuses a dotted reference (sql-driver.ts:18030, the 2026-08-06 same-table ruling). So B′ reverses one of the two rulings whichever side compiles it: (i) the native strategy grows its own cross-field compile branch (two enforcement places, against 2026-08-12), or (ii) the engine path learns a one-hop join for the offset (against 2026-08-06). Neither is "no reversal". The seat's letter is therefore A; B′ stays on the table only with a ruling record amending one of those two sentences.Workload
packages/spec/src/data/filter.zod.ts{ $now: true, addDays: <int | { $field }> }; spelling the dev's), describe + refusal prose; regenauthorable-surface{N_days_ago}resolves today —resolveFilterTokensin the engine andtemporal-comparand-door.ts— to a literal instant, leaving the offset reference for the executor; the{ $field }pair validation (engine.ts:21700,having-filter.ts:91) gains the class rule "no base column, offset NUMERIC only"driver-sqlliteral_now + offset_col days—crossFieldOffsetExpr(sql-driver.ts:18249) already spells the day arithmetic on all three dialects (sqlitestrftime … ' days', mysqldate_add … interval … day, pgmake_interval) withCOALESCE(offset, 0)and truncation; the same-table/tenant/declared checks ofapplyCrossFieldComparisonapply as they are@objectstack/formulamatches-filter.tsresolveDayOffset+addWholeDaysexist (:948–961); the now anchor is one resolverdriver-mongodb$fieldcomparands incl.addDaysare already classified (mongodb-filter.ts:486); one case to pin (compile or refuse)service-analytics$fieldcomparand doesNativeSQLStrategyfor this comparand with its own class/tenant checks (duplicating driver-sql's), join viaqualifyAndRegisterJoin; parity tests native ↔ engine (infer-cube-where-spelling-parityis the existing shape); a ruling recordvalidate-filter-tokens/ preset comparands learn the shape;content/docsfilter + query-syntax pages (precedent +64 lines);cross-field-conformance-cases.tscells on both paths (precedent +343)clm_contractcopied from the type on create / type change (hook or lookup-copied field): S in hotclmdomain:specwith driver-sql / formula hunks, contract-review tier; 2–3 dev-daysPerformance
review_started_at < now − sla_days dayshas a right-hand side that varies per row, so no index range scan onreview_started_atcan serve it (same on pg / mysql / sqlite). That is the same class as today'saddDayscolumn offset, and it is harmless where this card uses it: a dataset measure or dataset filter aggregates over the tenant-scoped row set anyway (COUNT/GROUP BYscans those rows with or without this predicate), so the added cost is one date arithmetic per scanned row — microseconds; at 10⁵ contracts, tens of milliseconds at most, under the aggregate's own cost. It matters only if the same predicate became the sole selective filter of a list view on a large table, which is not this card.review_due_at < now) is sargable — that is the one performance argument for C, and it buys nothing for an aggregate.LEFT JOINto the type table per query (primary-key lookup on a small table, hash or nested loop): the same join ADR-0071 already emits whenever a dimension names a path; negligible. Under (ii) the join moves to the engine path with the same cost.$fieldcomparand already sends a dataset query to the engine path (2026-08-12); for SQL drivers that path still compiles the WHERE in SQL throughdriver-sql(not re-read line by line here — stated as the ruling's own description), for the memory driver it evaluates per row inin-memory-aggregation.ts/matches-filter.ts, O(rows), where every dataset already runs.COALESCE), so a contract with no SLA days counts as overdue the momentreview_started_at < now; the dev pins that cell and the docs say it.维护者速读
addDays,+1816/−36、17 文件)的六到八成:spec 一个比较值 + 引擎的现在锚解析(复用{N_days_ago}的解析点)+ driver-sql 复用已有的三方言天数偏移表达式 + formula 复用已有的偏移解析 + 语料与文档,约 400–500 行源码、一个 PR、2–3 个 dev-day;应用侧 hotclm 要在合同上加一列 SLA 天数(S)。B′(偏移取关联记录)= A 再加数据集原生策略一条编译分支与双路径一致性测试,约 1.5–2 倍、4–5 个 dev-day,并且无论放在哪一侧都要推翻一条既有裁决(2026-08-12「跨列比较只在引擎路径一处执行」或 2026-08-06「SQL 下推只认同表列」),本席上一轮「B′ 不翻既裁」的前提被代码证伪,改荐 A。25808 A或25808 B′。objectstack-fleet commentedon Oct 26, 2026
Ruling: batch #395 item 3 · letter A · maintainer 「A」 2026-10-26T12:12Z
Recorded by the director seat (summon #51 continuation,
session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708; brief 6413864417, workload and performance reading 6414128401). The maintainer answered 「A」 after that reading. Letter A: the filter grammar gains a now-anchored comparand with a whole-day offset read from a numeric column of the same record (spelling the dev's to propose, e.g.{ $now: true, addDays: <integer | { $field }> }; sign rules asaddDays), usable as a datasetfilterand a dataset measurefilter, and as an ordinary data filter under the same rules. Both standing rulings hold unchanged: SQL push-down compiles same-table columns only (2026-08-06,filter.zod.ts), andNativeSQLStrategydeclines every cross-field comparand so the engine path enforces the cross-field rules in one place (2026-08-12,analytics-service.ts:4176). Not B′ (reverses one of those two) and not C (reverses hotclm#31 C).What the dispatch carries (from the reading 6414128401):
packages/spec/src/data/filter.zod.ts: the comparand shape, describe and refusal prose,authorable-surfaceregenerated;Clause-②: yes (widening),Metadata-protocol: added,minoron@objectstack/spec.{N_days_ago}resolves (resolveFilterTokens/temporal-comparand-door.ts) to a literal instant; the{ $field }pair validation (engine.ts~21700,having-filter.ts~91) gains the class rule "no base column; the offset column is NUMERIC, declared, same-table, never the tenant column".driver-sql:literal_now + offset_col daysthroughcrossFieldOffsetExpr(sql-driver.ts:18249) on all three dialects, withapplyCrossFieldComparison's checks as they are; NULL offset = 0 days (COALESCE), pinned and documented — a row with no SLA days counts as overdue once the anchor passes.@objectstack/formulamatches-filter.ts: the now anchor besideresolveDayOffset/addWholeDays;driver-mongodb: one pinned cell (compile or refuse).cross-field-conformance-cases.tscells on both paths; lint (validate-filter-tokens/ preset comparands);content/docsfilter and query-syntax pages.review_sla_daysonclm_contract(copied from the type on create / type change); hotclm#31 is told the platform answer.Workload of the chosen letter (reading 6414128401): about 0.6–0.8 of precedent PR #15102 — ~400–500 source lines, one PR in
domain:specwith driver-sql / formula hunks, contract-review tier, 2–3 dev-days. Performance: the predicate is non-sargable on the datetime column like today'saddDaysoffset; in a dataset aggregate it adds one date arithmetic per scanned row, under the aggregate's own cost.Execution:
needs-user-decision→pm:queuein this act (domain:spec, p1,area:reports). The spec seat dispatches one card thread withRuling-ref:naming this comment; the dev verifies premises 1–3 of the escalation 6409808781 first (no now-anchored column offset today; dotted$fieldrefused; PR #25903 touched nofilter.zod.ts) and reports a fork if any fails — ⛔ no forcing, ⛔ no path-valued offset slipped in.