fix(formula,spec,core): the RLS write-side check evaluator honours calendar-day upper bounds (ADR-0053 D-D) - #4076
Merged
Conversation
…lendar-day upper bounds
matchesFilterCondition — the evaluator behind RLS write-side `check`
policies (ADR-0058 D4) — compared a bare YYYY-MM-DD $lte bound
literally. On a datetime post-image a policy of the shape
{ signed_on: { $lte: '{today}' } } therefore DENIED every write made
after 00:00: the write-side twin of the read-side loss #3777 fixed, and
the last of the platform's filter backends that disagreed about what a
bare day denotes as a bound. The failure direction is what makes it
worth its own change — a rejected write, not a missing row.
$lte and a $between max now evaluate half-open against the next
calendar day, matching the SQL compiler, the memory and mongo drivers
and the analytics preview evaluator. Unchanged per the same semantics
table: full-ISO bounds keep exact-instant semantics, $gte/$gt/$lt keep
their midnight anchoring, a plain YYYY-MM-DD value compares identically
(string ordering makes the forms equivalent), and the evaluator stays
fail-closed on a null bound.
Where the rule lives: nextUtcCalendarDay moves from @objectstack/core to
@objectstack/spec/data, beside date-macros.zod.ts whose vocabulary it
interprets — the macros define which bare days an author can NAME, this
defines what such a day DENOTES as a bound, and both are protocol. Same
species as the pure helpers spec/data already owns, so PD#2 holds.
Chosen over @objectstack/types because it adds no dependency edge: every
consumer already depends on spec, while core does not depend on types.
core re-exports the symbol, so the published surface the drivers and
analytics strategies import from is unchanged.
Rejected: a private copy in formula — the precedent it would cite
(formula's own today()) is debt, not a pattern, and a second copy of
this rule is exactly the divergence #3777 catalogued. Six backends now
share one definition.
Tests: 10 alignment cases in formula (whole-day admit, next-day stop,
plain-date equivalence, full-ISO not widened, gte/gt/lt invariance,
$between min still bounded, impossible day not rolled over, null bound
fail-closed); the primitive's thorough coverage moves to spec, with core
keeping a re-export gate. spec 6943 / core 414 / formula 273 /
driver-sql 508 / driver-memory 194 / driver-mongodb 107 /
service-analytics 329, all green. ADR-0053 gains D-D2 and its D-E4 open
item is closed.
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 3 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The comparand table said a bare YYYY-MM-DD on a datetime field means "midnight UTC" full stop. That is true for $gte/$gt/$lt and wrong for $lte and a $between max, where the day denotes the whole day and compiles half-open (ADR-0053 D-D). Documents the operator split, when to pass a full ISO instant instead, and that the rule holds on every backend including the RLS write-side check evaluator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
…darDay The API-surface gate diffs spec's built exports against a committed snapshot; adding `nextUtcCalendarDay` to ./data made it stale and turned the TypeScript Type Check job red. Regenerated with the gate's own `gen:api-surface`, so the diff is exactly the one addition — 0 breaking, 1 added, which is what a new export should read as. 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:11
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…DR-0053 D-A3) (#4098) * feat(spec,service-analytics): a shared temporal conformance matrix (ADR-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 * docs(service-analytics): list $between in the preview evaluator's declared 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
收尾 #3777 / #4042 / #4047 那条线上最后一个未对齐的求值面。无 issue —— 它是 ADR-0053 D-E4 明确记录的未决项,本 PR 关闭它。
为什么值得单独一个 PR:这一面的失败方向是「拒绝写入」
matchesFilterCondition是 RLS 写侧check策略(ADR-0058 D4)的求值器。它对裸YYYY-MM-DD的$lte做字面比较,所以打在datetimepost-image 上时,{ signed_on: { $lte: '{today}' } }这类策略拒绝掉当天 00:00 之后的每一次写入。前面三个 PR 修的都是读窗口「少了几行」;这一面错了是策略判定错——本该放行的写被拒(或反向)。这也是平台五个 filter 后端里最后一个对「裸日期作上界是什么意思」持不同意见的。
变更
$lte与$between的 max 改为对次日半开求值,与 SQL 编译器、memory/mongo 驱动、analytics preview 一致。刻意不动(同一张语义表):完整 ISO 边界保持精确瞬时、$gte/$gt/$lt保持午夜锚定、纯YYYY-MM-DD值比较结果不变(字符串序下两种形式等价)、null 边界仍 fail-closed。归属决策(这是本 PR 的实质部分)
nextUtcCalendarDay从@objectstack/core迁到@objectstack/spec/data(新文件calendar-day.ts),紧邻date-macros.zod.ts。理由:两者是同一份契约的两半——macros 定义作者能命名哪些裸日期,D-D 定义这样一个日期作为边界指代什么。这是协议,不是业务逻辑,与
spec/data已有的parseDateMacroParam/canonicalAstOperator/parseAutonumberFormat同species,因此 Prime Directive #2 成立。选 spec 而非
@objectstack/types的决定性理由:零新增依赖边。所有消费者已依赖 spec;而core目前不依赖types,走 types 需要新建一条core → types边。拒绝在 formula 里放私有拷贝:它能援引的先例(formula 自己的
today())是债不是模式,而这条规则的第二份拷贝正是 #3777 编目的那种分歧。现在六个后端共用一个定义。无需改动任何 import:
core继续再导出该符号,驱动与 analytics 策略现有的from '@objectstack/core'照常工作;新代码建议直接用@objectstack/spec/data。覆盖
$gte/$gt/$lt不变性、$between下界仍生效、不可能日期(2026-02-30)不被翻滚、null 边界 fail-closed。calendar-day.test.ts,含闰年/月末/年末翻转与全部拒绝分支);core 保留一条再导出门——它断言的是驱动与策略实际 import 的那个surface,去掉会在别处才报错。测试
spec6943 ✅ /core414 ✅ /formula273 ✅(含新 10)/driver-sql508 ✅ /driver-memory194 ✅ /driver-mongodb107 ✅ /service-analytics329 ✅ —— 七个包全绿,既有断言零改动(迁移对下游是透明的)。三包带 dts 构建 ✅;改动文件 eslint 干净(spec/**不在 eslint 配置范围内,是既有状况,非本 PR 引入)。关联
🤖 Generated with Claude Code
https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
Generated by Claude Code