Skip to content

fix(formula,spec,core): the RLS write-side check evaluator honours calendar-day upper bounds (ADR-0053 D-D) - #4076

Merged
os-zhuang merged 3 commits into
mainfrom
claude/dashboard-date-range-data-loss-b7qt5o
Jul 30, 2026
Merged

fix(formula,spec,core): the RLS write-side check evaluator honours calendar-day upper bounds (ADR-0053 D-D)#4076
os-zhuang merged 3 commits into
mainfrom
claude/dashboard-date-range-data-loss-b7qt5o

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

收尾 #3777 / #4042 / #4047 那条线上最后一个未对齐的求值面。无 issue —— 它是 ADR-0053 D-E4 明确记录的未决项,本 PR 关闭它。

为什么值得单独一个 PR:这一面的失败方向是「拒绝写入」

matchesFilterCondition 是 RLS 写侧 check 策略(ADR-0058 D4)的求值器。它对裸 YYYY-MM-DD$lte 做字面比较,所以打在 datetime post-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 编目的那种分歧。现在六个后端共用一个定义。

无需改动任何 importcore 继续再导出该符号,驱动与 analytics 策略现有的 from '@objectstack/core' 照常工作;新代码建议直接用 @objectstack/spec/data

覆盖

  • formula 10 条对齐用例:整天纳入、次日截断、纯日期值等价、完整 ISO 不放宽、$gte/$gt/$lt 不变性、$between 下界仍生效、不可能日期(2026-02-30)不被翻滚、null 边界 fail-closed。
  • 原语的详尽覆盖迁到 speccalendar-day.test.ts,含闰年/月末/年末翻转与全部拒绝分支);core 保留一条再导出门——它断言的是驱动与策略实际 import 的那个surface,去掉会在别处才报错。

测试

spec 6943 ✅ / core 414 ✅ / formula 273 ✅(含新 10)/ driver-sql 508 ✅ / driver-memory 194 ✅ / driver-mongodb 107 ✅ / service-analytics 329 ✅ —— 七个包全绿,既有断言零改动(迁移对下游是透明的)。三包带 dts 构建 ✅;改动文件 eslint 干净(spec/** 不在 eslint 配置范围内,是既有状况,非本 PR 引入)。

关联

🤖 Generated with Claude Code

https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f


Generated by Claude Code

…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
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 8:45am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/formula, @objectstack/spec.

114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/core)
  • content/docs/permissions/authentication.mdx (via @objectstack/core)
  • content/docs/permissions/authorization.mdx (via packages/core, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/formula, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/core, @objectstack/formula)
  • content/docs/releases/v16.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 30, 2026 08:34
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
os-zhuang marked this pull request as ready for review July 30, 2026 09:11
@os-zhuang
os-zhuang merged commit 2af1988 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/dashboard-date-range-data-loss-b7qt5o branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants