feat(spec,service-analytics): a shared temporal conformance matrix (ADR-0053 D-A3) - #4098
Merged
Merged
Conversation
…DR-0053 D-A3) The last decision in ADR-0053 that had never been actioned, and the regression backstop the four previous incidents lacked. #3650 (window dropped), #3773 (buckets collapsed to NULL), #3777 (bare-day upper bound dropped the final day), #4047 (cross-type comparison matched nothing) were each found by a human by accident, and each left a suite proving only ITS OWN issue against its own fixture. That is coverage without a standard: nothing could fail when two backends drifted apart. `spec/data/temporal-conformance.ts` exports TEMPORAL_ROWS (times of day chosen so a midnight-anchored bound and a whole-day bound cannot return the same set) and TEMPORAL_CASES (filter + expected row ids + a note naming the incident each case defends). Five backends consume it and assert ROW RESULTS through their real entry points — the ADR's original requirement: - driver-sql (typed; the live-dialect CI job also runs it against real Postgres and MySQL under a skewed process zone, no workflow change) - driver-memory, driver-mongodb (real MongoDB via mongodb-memory-server) - service-analytics preview evaluator, formula matchesFilterCondition Modelled on filter-logic-conformance.ts, which exists for the same reason one layer down (#3774); in spec for the D-D2 reason — every backend already depends on it, so this adds no dependency edge. The matrix found two things on its first run, which is the argument for having built it: 1. A real defect. The preview evaluator had no $between case, so it fell through to its permissive `default: return true` and matched EVERY row — a drafted dashboard with a range filter charted the whole dataset, then changed at publish, which is the exact continuity the preview exists to provide. Fixed here, sharing the upper-bound helper with $lte so the whole-day rule covers a range's max too. 2. A measured, irreducible limit. $gt with a bare-day comparand on a datetime column cannot agree across backends: a typed one anchors to midnight and excludes a 00:00 value, a type-blind one compares raw strings where '…T00:00…' sorts after the bare day and keeps it. The upper-bound trick has no lower-bound analogue — anchoring would fix datetime and break date. So $gt is shared on `date` only and the datetime cell stays in the typed drivers' suites; $gte IS shared, and is pinned precisely because the two readings agree by luck of the boundary being inclusive. Also: AGENTS.md gains the Post-Task step for regenerating the spec API-surface snapshot — OS_SKIP_DTS=1 skips the built declarations that gate reads, so it passes locally and fails in CI, which is how #4076 went red. spec 6957 / formula 286 / driver-sql 535 / driver-memory 207 / driver-mongodb 120 / service-analytics 342, all green. Closes #4081 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 6 package(s): 113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…lared scope The module's scope comment enumerated the operators the preview supports and omitted $between — accurately, since it had no case and fell through to the permissive default. Now that it has one, the list has to say so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
os-zhuang
marked this pull request as ready for review
July 30, 2026 09:52
os-zhuang
pushed a commit
that referenced
this pull request
Jul 30, 2026
…n top Main landed its own D-A3 matrix (#4098: TEMPORAL_ROWS/TEMPORAL_CASES + the preview $between fix) while this branch was in flight. Resolution keeps main's landed table, naming and consumers as the base, and re-applies this branch's increments — exactly the coverage #4098's ADR addendum recorded as still open: - the relative-token axis: TemporalCase.tokenFilter/dateRange + TEMPORAL_NOW, resolved via @objectstack/core resolveFilterTokens in the five core-reachable consumers (formula stays literal-only by dependency stance) - TemporalRow.writerForm: mixed writer-population seeding (D-E4) in the sql/wasm/memory/mongo consumers - a pre-epoch fixture row, and the date equality/$in cells (#1874's shape) - a driver-sqlite-wasm consumer pinning the inherited SqlDriver seam - a legacy-storage sweep in the driver-sql consumer (un-backfilled mixed epoch/naive column through the read-repair path) The duplicate preview consumer from this branch is dropped in favour of main's preview-temporal-conformance.test.ts, extended with the token and timeDimensions.dateRange sweeps; this branch's changeset is rewritten as the increment (.changeset/temporal-conformance-token-axis.md) alongside #4098's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqqZmPS5a4gJGBoCTwipFr
This was referenced Jul 30, 2026
os-zhuang
pushed a commit
that referenced
this pull request
Jul 30, 2026
…$between it found (ADR-0053 D-A3.1) NativeSQLStrategy is the surface #3650 was actually about, and it was listed in the conformance matrix's own backend table with no consumer — because every existing suite for it asserts the emitted SQL string, and a DROPPED predicate is invisible to a string assertion: the SQL stays valid, just wider. Executing the shared cases against a real engine (sql.js, the same pure-WASM engine driver-sql falls back to) and asserting row ids is what D-A3 asked for, and it immediately showed `$between` returning the entire table. The cause sat one layer below the strategy, in the shared filter-normalizer: `$between` was absent from MONGO_TO_CUBE_OP, and an unmapped operator is `continue`d, so the predicate vanished from the compiled WHERE clause. Both strategies read that normalizer, so the ObjectQL aggregate path was affected too. User-visible symptom: a widget with a range filter charts the whole dataset, with nothing in the SQL to suggest a filter was ever requested. `$between [min, max]` now LOWERS to its two bounds (gte + lte) rather than gaining an operator of its own, so a range's max inherits the calendar-day whole-day rule (#3777) from each strategy's existing upper-bound handling — NativeSQLStrategy compiles a bare-day upper bound half-open itself, the ObjectQL path gets the same rule from the driver — instead of needing a second implementation to keep in step. That is how #4098 closed the same defect on the preview evaluator. A malformed `$between` throws rather than being dropped, the stance driver-memory took for the same shape in #3948. The consumer runs the literal, relative-token and timeDimensions.dateRange spellings of every shared case. Rows are seeded canonical, where the driver's temporal hooks are identities, so the context omits them and exercises the same "absent = identity" path Postgres takes (D-A2); the un-backfilled mixed column stays covered where the driver truth lives. The cause itself is NOT fully closed: `$startsWith`, `$endsWith`, `$null` and `$regex` are still silently dropped by the same `continue`. Filed as #4128 rather than expanded into this change, with the #3948 precedent for turning the fallback into a throw. service-analytics 391 green (was 360; +31 from the new consumer). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqqZmPS5a4gJGBoCTwipFr
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…onsumer, and the dropped predicates it found (ADR-0053 D-A3.1) (#4109) Completes ADR-0053 D-A3 on top of the matrix #4098 landed, closing the two coverage gaps that addendum recorded as still open — and fixing the defects found while closing them. The relative-token axis: TemporalCase gains tokenFilter/dateRange plus the pinned TEMPORAL_NOW, so a case spelled in {today} / {90_days_ago} / period tokens must reach the same rows as its literal twin — resolver drift and evaluator drift become distinguishable. Also TemporalRow.writerForm (D-E4 mixed-writer seeding), a pre-epoch row, the date equality/$in cells (#1874's original shape), a driver-sqlite-wasm consumer pinning the inherited seam, and a legacy-storage sweep in the driver-sql consumer. The sixth consumer: NativeSQLStrategy — the surface #3650 was about — was in the matrix's backend table with no consumer, because its existing suites assert the emitted SQL string, and a DROPPED predicate is invisible to that: the SQL stays valid, just wider. Executing the cases against a real engine and asserting row ids found $between returning the entire table, caused by the shared filter-normalizer skipping any operator missing from its map. Closing that cause rather than the instance surfaced four more, two of them inversions rather than omissions: $startsWith / $endsWith / $null dropped ($null is what the console emits for an "is empty" filter, so those widgets showed every row); $exists mapped value-independently, so {$exists: false} compiled to IS NOT NULL; and $notContains reaching ObjectQLStrategy without an arm, compiling "does not contain x" as "equals x". Unknown operators now throw on both surfaces rather than being dropped or reinterpreted (#3948's call). $or / $not remain skipped, now declared in the module doc rather than silent. Closes #4128. Refs #4081, #4098.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4081。ADR-0053 里最后一个从未被落实的决策,也是前四次事故都缺的那道棘轮。
为什么现在做
这条接缝坏了四次,每次都是有人偶然撞到:#3650(窗口被整个丢弃)、#3773(分桶塌成 NULL)、#3777(裸日期上界丢当天,默认配置即命中)、#4047(跨类型比较恒 false,mongo 上窗口返回空集)。
每次修完都留下一个「证明自己那个 issue」的测试文件,各带各的夹具。这是有覆盖但没有标准:两个后端一旦漂移,没有任何东西会红。
形状:一张共享表,不是各修各的套件
packages/spec/src/data/temporal-conformance.ts导出TEMPORAL_ROWS(时刻刻意选得让「午夜锚定」与「整天」两种边界不可能返回同一集合)与TEMPORAL_CASES(filter + 期望行 id + note 指名它防的是哪次事故)。五个后端通过各自真实入口消费并断言行结果(ADR 原文的硬性要求):driver-sqldriver-memory/driver-mongodbmongodb-memory-server)formulamatchesFilterCondition照抄
filter-logic-conformance.ts(它为低一层的同类问题 #3774 而建);放 spec 的理由同 D-D2——所有后端已依赖它,零新增依赖边。矩阵第一次运行就找到两件事(这就是建它的论据)
1. 一个真实缺陷。 preview evaluator 根本没有
$between分支,落到 permissive 的default: return true,匹配每一行——草稿 dashboard 上带区间过滤器的图表画的是全量数据集,发布后数字跳变,而这恰恰是 preview 存在的意义(草稿与发布数字连续)。本 PR 一并修掉,并与$lte共用上界 helper,使整天规则同样覆盖区间的 max。2. 一个实测的、不可消除的限制。
$gt配裸日期打在datetime列上,类型化与类型盲后端无法一致:前者把边界锚到午夜、排除恰好 00:00 的值;后者比较裸字符串,而'2026-07-28T00:00:00.000Z'在共同前缀上排在'2026-07-28'之后,于是保留它。上界那个类型盲技巧(
<= day改写成< nextDay)没有下界对应物:把下界也锚成…T00:00:00.000Z会修好 datetime 却弄坏 date('2026-07-28' >= '2026-07-28T00:00:00.000Z'为假)。所以
$gt在共享矩阵里只断言date列,datetime那格留给类型化驱动各自的套件;$gte是共享的——两种读法都含午夜行,但它们一致是边界恰好包含的运气而非设计,正因如此才要钉住。如实记录,不粉饰。顺带
AGENTS.mdPost-Task Checklist 补一步:改动 spec 导出后需gen:api-surface。该门读的是构建出的 dist 声明,而OS_SKIP_DTS=1(本地提速常用)恰好跳过它——所以本地全绿、CI 才红,#4076 就是这么红的一轮。测试
spec6957 ✅ /formula286 ✅ /driver-sql535 ✅ /driver-memory207 ✅ /driver-mongodb120 ✅ /service-analytics342 ✅ —— 五个后端对同一批 13 个 case 逐格给出相同行集。API 表面快照已重新生成(这次记得了)。仍然开放(有意)
relative-token 轴(
{today}/{30_days_ago}经filter-tokens.ts解析后端到端跑)尚未进表。当前 case 取的是已解析的比较值——四次事故实际发生的那一层。已记入 ADR-0053 D-A3.1 的 Consequences。🤖 Generated with Claude Code
https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
Generated by Claude Code