Skip to content

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

Description

@os-warren

Summary

A dataset measure (ui/dataset.zod.ts, ADR-0021) cannot express the single most common
governance metric there is: "was this completed by its deadline, where the deadline is
a stored date plus a grace period that lives on the parent record?"

The concrete comparison, from the duly application (objectstack-ai/duly#9):

completed_at <= due_date + duty.grace_days

completed_at and due_date are columns on the base object duly_task.
grace_days is a number column on the related duly_duty. All three are stored and
indexed. There is no spelling for this in the measure surface, so the on-time rate, the
"done on time" count and the "late" count all have to be dropped from the dataset.

Recorded as a capability gap rather than a bug — nothing misbehaves, and the refusal in
part 2 below is loud and correct. The gap is that the semantic layer cannot say the
thing, and the workarounds available to an application author are all worse than the gap.

What works, so the gap is precisely located

Reaching the related field works, and this is worth stating first — it is the half a
reader will assume is broken. include: ['duty'] plus a duty.<field> path is exactly
what the semantic layer is for: joins are compiled from include (ADR-0071, ≤3 hops) and
the author writes no ON clause. duly_duty_health ships a frequency dimension bound to
duty.frequency today and it validates, builds and appears in the artifact.

So a measure can read duty.grace_days. It cannot compare against it.

What does not work — two independent missing pieces

1. There is no date arithmetic anywhere in the filter grammar

FILTER_OPERATORS (data/filter.zod.ts) is closed: equality, ordering, set, range,
string, null/exists. Nothing adds an interval to a column.

The {N_days_ago} vocabulary is not a substitute — DATE_MACRO_PARAM_RE
(data/date-macros.zod.ts) is anchored to now, never to another column. {7_days_ago}
can express "untouched for a week"; nothing can express "due_date plus seven days".

And here the offset is not even a literal: it is itself a column (duty.grace_days),
which is strictly harder than a fixed interval.

2. Column-to-column comparison is declared but refused on the SQL path

Even setting grace aside, the grace-free form completed_at <= due_date has no usable
spelling either. FieldReferenceSchema declares it:

{ completed_at: { $lte: { $field: 'due_date' } } }

but filter.zod.ts's own "Execution support (#5041)" block records that driver-sql (and
driver-sqlite-wasm, which inherits its compiler) reject it with INVALID_FILTER / HTTP
400, while the in-memory evaluator resolves it. SQL compilation is tracked as #5222.

For a dataset that split is worse than a uniform gap. A dataset is a definition that
outlives any one query and is bound by name from dashboards and reports. A measure using
$field would answer on a memory-driver deployment and 400 on a SQL one — the semantic
layer's correctness would become a property of the deployment.

So even if #5222 landed tomorrow, part 1 would still leave due_date + duty.grace_days
unspellable. The two are additive, not alternatives.

Why the application-side workarounds were all rejected

Listing these because each one is what an author reaches for, and each one is how this
gap would have become permanent and invisible:

  • Denormalise grace_days (or a pre-computed grace_deadline) onto the task. A second
    writer that drifts the day a duty's grace is edited. Forbidden by the app's own rules and
    wrong regardless — it converts a definition into a snapshot.
  • Reduce the rate in TypeScript over query results. Puts the number outside the
    semantic layer, where no dashboard widget can bind it — which defeats the point of
    ADR-0021 — and duplicates the definition per consumer.
  • Ship an on-time rate that silently ignores grace. Wrong in the direction that
    matters: it marks late every task completed inside the grace its own duty grants. And
    wrong invisibly, which is how a number nobody trusts becomes the number everybody
    reports.

The application therefore ships the dataset without the on-time measures rather than
with approximated ones.

What the gap currently costs, concretely

In duly, grace_days is authored on duly_catalog_item, propagated to duly_duty at
instantiation, and read by nothing. This dataset was its only intended consumer. It is
a declared, populated, propagated field with zero enforcement — an ADR-0049
declared-but-unenforced shape created purely by the absence of a way to consume it.

Possible directions (not a proposal — the semantics need a ruling)

Sketching these only to show the design space is not empty:

  1. An offset on the field reference — { $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } }.
    Smallest surface; rides on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222; keeps the layer SQL-free.
  2. A derived-measure operator over a date difference — the layer already has
    derived: { op, of } for combining measures by name; a days_between measure plus an
    ordering test would be a natural extension of that vocabulary.
  3. A declared computed dimension on the dataset — riskier, since the whole point of
    ADR-0021's author surface is that it is smaller than a query and carries no SQL.

Whichever way it goes, the NULL semantics need stating (a duty with no grace, a task with
no due date) — the same care #5146 took over $not.

Reproduction

objectstack-ai/duly at claude/issue-9-analytics-datasets, pnpm validate. Add to
src/datasets/duty-health.dataset.ts:

{
  name: 'tasks_done_on_time',
  aggregate: 'count',
  filter: {
    source: { $in: ['catalog', 'assigned'] },
    status: 'done',
    completed_at: { $lte: { $field: 'due_date' } },   // no way to add duty.grace_days
  },
}

This parses and validates clean — the schema accepts $field — and then refuses at
runtime on any SQL driver. There is no variant of it that adds the grace offset at all.

Environment

@objectstack/spec 17.2.0, @objectstack/cli 17.2.0.

Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
dataset with the affected measures omitted rather than approximated.

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): needs-user-decision · priority:p2 · domain:spec · type Feature. Reason for the inbox: date arithmetic in the filter grammar / a new measure operator is a published-contract shape addition — the human floor. 决策分析(中文,按四维模板):

    <!-- os-decision-facets -->
    一句话问题:「这件事是否在期限内完成」—— 期限 = 到期日 + 上级记录上的宽限天数 —— 今天在数据集里写不出来,应用只好把准时率、按时数、逾期数三项指标整个删掉。

    前提(每条带 re-check):

    • 前提 1:过滤词表是闭合集且无日期算术 —— git grep -n "FILTER_OPERATORS" origin/main -- packages/spec/src/data/filter.zod.ts(阳性对照:同文件 $lte 非 0)。
    • 前提 2:$field 列比列在 SQL 驱动上以 INVALID_FILTER / 400 拒绝、memory 驱动可算 —— git grep -n "#5041" origin/main -- packages/spec/src/data/filter.zod.ts。
    • 前提 3:[spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222(SQL 端编译 $field)的现状 —— 现读 open/closed,⛔ 不引本卡写作时的描述。

    选项 × 真实代价:

    选项 做什么 客户可感知的代价
    A 字段引用上加偏移:{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } },骑在 #5222 上 最小面、同一条契约线;但 #5222 不落地则 SQL 上仍 400,应用继续删指标
    B 派生度量加「日期差」算子 + 排序测试(复用 derived: {op, of}) 语义层多一个算子;NULL 语义(无宽限的 duty、无到期日的 task)要写明
    C 数据集上声明计算维度 最灵活也最危险 —— ADR-0021 作者面的本意是「比查询小、不带 SQL」
    D 不加,应用侧 TypeScript 归约(现状) 准时率不能被任何看板绑定;每个应用各写一份定义

    业务含义直译:A = 像 SQL 的 due_date + interval;B = 像 BI 工具里的「日期差」度量;C = 像自定义 SQL 列;D = 把 KPI 移出语义层。

    四轴(业务立场;①权重 ≥50%):
    ① 项目长远合理性:A 最小、与 #5222 同一条契约线,⛔ 不新增第二种日期语义;C 扩大契约增生。⇒ 荐 A(以 #5222 为前置)。
    ② 实际业务拉动:有 —— duly#9 因此删掉三项指标;期限 / SLA / 宽限是治理类应用最常见的度量。
    ③ 防 AI 犯错:A / B 让未定义组合在 validate 时响亮拒绝;D 是「忽略宽限的准时率」这种静默错数的温床。
    ④ 创业阶段不扩散:A 只加一个键;⛔ C 不做。

    推荐:A(前置 #5222;NULL 语义按 #5146 对 $not 的写法一并写明),回退 D 记录在案(不做,等第二个应用拉动)。置信缺口:#5222 的现状与成本本席未读;A 在 memory 驱动求值路径上的行为未测。

    裁后执行(你不用管):A ⇒ 若 #5222 仍 open 则转 pm:blocked + Blocked-by: #5222,否则 pm:queue + domain:spec,派发 Clause-② yes(扩公开面);D ⇒ 关 not planned 留记录。


    Generated by Claude Code

  2. os-project-manager commented on Sep 2, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling recorded — A: an offset on the field reference

    Director seat, summon #8, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #4 in which this card was item 1 with the recommendation A (fallback D). Verbatim reply: 「13973 帮我综合分析,并参考主流平台的方案,并给我解释为什么不能都用日期类型。其他同意」 — 「其他同意」 adopts A.

    Premise correction carried into the ruling. The card's part 2 ("column-to-column comparison is refused on the SQL path, tracked as #5222") is stale: #5222 closed completed on 2026-08-11 via PR #7582, so $field already compiles to a column-to-column comparison on driver-sql. The only remaining gap is date arithmetic. If the "Execution support (#5041)" block in filter.zod.ts still says the SQL path refuses $field, that prose is out of date and moves with this card.

    Ruled: A. FieldReferenceSchema gains an offset: { $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } } (a literal number is also accepted where a column is). NULL semantics are stated in the schema description and pinned, in the shape #5146 used for $not: a NULL offset column contributes zero days; a NULL due_date makes the comparison false (no deadline, never "on time"), both spelled out rather than inherited from SQL three-valued logic. Compiles on every dialect the $field compiler already covers and evaluates identically on the memory evaluator; the dataset conformance pin drives the duly shape (completed_at <= due_date + duty.grace_days) on both. Options B (derived date-diff measure) and C (computed dimension) are not taken; D (app-side reduction) is superseded.

    Execution: domain:spec lane; Clause-②: yes (the filter grammar widens), dispatch at CONTRACT_REVIEW_TIER. The dispatch order names the #5222 landing (PR #7582) as the base to extend and requires the memory-evaluator leg alongside the SQL one.

    State transition, same stroke: needs-user-decision → pm:queue. Ledger: director seat post #12708, summon #8.


    Generated by Claude Code

  3. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Claim: PM loop, domain:spec seat (seat post #6017), R3 of this shift — the cross-domain single PR the ruling designates to this lane (spec grammar + the driver-sql compiler + the memory evaluator)
    Session: session_0174WZTU6XcFcS7g2kykC53i (GitHub zhuangjianguo)
    Branch: claude/issue-14104-field-reference-add-days
    Worktree: objectstack-issue-14104 (inside the cloud session's own container)
    Domain: domain:spec (designated cross-domain: packages/spec + packages/drivers/driver-sql + packages/formula, one PR)
    File surface (declared in full, as the cross-domain path requires):


    Generated by Claude Code

  4. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Dispatch (R3 of this shift, 2026-09-03T20:46Z) — domain:spec seat, session_0174WZTU6XcFcS7g2kykC53i, seat post #6017. mode:cloud (L is the cloud-reserved tier), model: fable (CONTRACT_REVIEW_TIER), size L, Clause ② yes (dual carrier hung with the PR). Cloud session session_019vx3536MUFc8XYVLNoKhs3 (created 20:45Z, outcome branch claude/issue-14104-field-reference-add-days; the archive obligation is this seat's). The dev leaves its own Claim: comment below carrying the Clause-②: yes line; the assignee stays the PM's.

    Dispatch text handed to the dev, in brief (ruling 5507665791's ruled shape, NULL semantics, "PR #7582 is the base" and "memory leg beside the SQL leg" are quoted verbatim and marked non-renegotiable; the premise correction that #5222 is closed is carried):

    • Spec: FieldReferenceSchema gains addDays (integer literal of any sign, or a nested { $field } reference); describe states the NULL semantics; the stale "Execution support ([spec] $field 跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041)" docblock rewritten to the landed state; no new positions; pins with code + path + first sentence; generated artifacts via --fix; query-syntax.mdx gains the offset.
    • SQL: the PR feat(driver-sql): compile $field to column-to-column comparison (#5222) #7582 cross-field arm compiles column OP (other + COALESCE(offset, 0) days) under other IS NOT NULL AND … on SQLite / PostgreSQL / MySQL so a NULL base is false and stays false under $not; the shared conformance corpus gains the offset rows (literal, column, negative, NULL offset, NULL base, $not), the driver-sql and driver-sqlite-wasm suites run them, the live PG + MySQL leg goes where that CI job runs.
    • Memory: matches-filter.ts resolveValue adds whole days with identical NULL semantics; pins from the same rows.
    • Read-and-measure with a pass-through fix in scope: service-analytics comparand-shape.ts isFieldReference (the detector that routes a dataset cross-field filter to the engine path per the 2026-08-12 ruling) must keep detecting a reference that carries addDays; the temporal comparand doors unchanged. Dataset-level pin of the duly shape beside the existing analytics dataset tests.
    • Changesets: spec / driver-sql / formula minor; census --fix; H17: touching sqliteCanonicalDatetimeSql or backfillCanonicalDatetimes must be declared in the PR body (drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009), no rider work.
    • Stops: SQLite canonical datetime ambiguity (text vs julian); the analytics detector needing more than a pass-through (then Part of #14104 + the fork stated); any file outside the declared surface.
    • Landing: draft PR Fixes #14104, two-limb Clause ② line, needs:contract-review on PR and card in the same stroke with readbacks, ⛔ no ready flip; report as os-dev-report here. Self-drive clause for the cloud session: no turn ends on a question or an interim report.

    Generated by Claude Code

  5. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Claim: os-dev developer agent, dispatched by the domain:spec PM seat (seat post #6017, dispatch 5531920565)
    Session: session_019vx3536MUFc8XYVLNoKhs3 (cloud, mode:cloud, size L)
    Branch: claude/issue-14104-field-reference-add-days (pushed empty at 1c7adc7 = origin/main as the write-route probe; probe answered 200)
    Worktree: ../objectstack-issue-14104, base 1c7adc73 (origin/main at claim time, after PR #15054's fc58a99e landing on filter.zod.ts)
    Ruling followed: maintainer ruling A as recorded in 5507665791 — FieldReferenceSchema gains addDays (integer literal of any sign, or a nested { $field } reference), NULL semantics stated and pinned, SQL leg on the PR #7582 cross-field arm, memory leg in matches-filter.ts, conformance corpus rows on both paths, dataset pin of the duly shape.
    File surface: exactly the one declared in the PM claim 5531891703 (spec grammar + pins + generated artifacts + query-syntax.mdx; driver-sql compiler + shared corpus + the two driver conformance suites + driver-sqlite-wasm's inherited run; formula evaluator + pins; a read-and-measure of service-analytics' isFieldReference detector with a pass-through fix in scope; the analytics dataset pin; changesets; census).
    Clause-②: yes
    Assignee: the PM's; not touched by this seat.


    Generated by Claude Code

  6. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    os-dev-report

    {
      "issue": 14104,
      "status": "done",
      "branch": "claude/issue-14104-field-reference-add-days",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15102",
      "premise_still_valid": true,
      "summary": "Ruling A implemented end to end at b45c078: FieldReferenceSchema gains addDays (integer literal of any sign, or a nested { $field } reference, dot-path allowed) with the ruled NULL semantics stated in the describe and refused shapes named at the schema door and repeated at the ordering slot; driver-sql compiles the offset on the PR #7582 cross-field arm per dialect (SQLite date()/strftime on the canonical text form, Postgres date+int / make_interval, MySQL date_add) as a total predicate (COALESCE(offset,0), referenced IS NOT NULL so a NULL base is false for every operator including $ne and stays false under $not); matchesFilter evaluates it identically; a second nine-row conformance fixture (date pair + datetime pair + nullable numeric grace, 42 cases + 10 refusals) is held to the same ids on driver-sql, driver-sqlite-wasm and the analytics face; the analytics detector was measured NOT key-count-strict (no pass-through fix needed) and pinned, the objectql and core temporal doors measured never-judged (not edited); a dataset-level pin drives the duly shape through queryDataset on a real wasm engine (2 on time, 4 late, rate 2/9). Premise note: the card's part 2 (SQL refuses $field) was stale, as the ruling already recorded — the docblock is rewritten to the landed state. The ruling's literal dotted offset (duty.grace_days) is refused on SQL push-down under the 2026-08-06 same-table ruling and answered in memory; pinned as a loud asymmetry, recorded below. PR is draft with needs:contract-review hung on PR and card (both read back); the H17 mention of sqliteCanonicalDatetimeSql (a docblock link, no code change) is declared in the PR body.",
      "tests": "At b45c078 (worktree, under scripts/pm/os-verify-lock.sh, exits captured before any pipe): pnpm --filter @objectstack/spec test → Test Files 465 passed, Tests 12408 passed, exit 0; driver-sql test → 152 passed/9 skipped files, 2345 passed/140 skipped tests (skips = unprovisioned live PG/MySQL cells, named by declareUnprovisionedCell — NOT MEASURED locally, CI's Temporal Conformance job runs them), exit 0; driver-sqlite-wasm test → 26 files/501 tests passed; formula test → 28 files/792 tests passed; service-analytics test → 89 files/1968 tests passed; typecheck ×5 (spec incl. check:test-typecheck OK, driver-sql, formula, driver-sqlite-wasm, service-analytics) exit 0. Spec: check:generated second run (no --fix) '✓ All 15 generated artifacts are up to date.'; check:authorable-surface '✅ Successfully generated 1600 schemas.'; check:liveness exit 0; check:docs '✅ 229 generated files in sync'; check:api-surface 'unchanged ✓'; check:skill-examples '✅ 257 prose examples type-check' (covers the new os:check docs block). Census: --fix '0 anchor(s) rewritten', plain 'OK — 106 elevation read sites … all anchored'. eslint --no-inline-config over the 10 edited TS files: exit 0. check:doc-authoring green after stripping tracker ids from three new customer-facing strings (negative pin added). dispatch-gates union (79 commands from the 17-path change set vs merge base 1c7adc7): 74 green by their own verdict lines; NOT MEASURED by their own text: check-dev-prereqs (exit 1, stale/absent dist of unrelated packages, asks for full pnpm build), check-test-completeness (exit 3 PREREQUISITE NOT MET, needs a turbo test log), check:dual-build-cjs-loads (exit 3 PREREQUISITE NOT MET), check:type-check-debt (exit 3 PREREQUISITE NOT MET), check:entry-nameability (exit 0, prints NOT MEASURED for @objectstack/spec/qa, pre-existing). Reverse verification, both legs scripted with trap restore and blob-hash proof: leg 1 (SQL offset compile unhooked, marker count 1 on disk) → driver-sql cross-field conformance 43 failed/74 passed/2 skipped, all reds in the [addDays] blocks (17 'SQL push-down disagreed' + refusal arm 'expected a refusal'), 0 memory disagreements, formula pins 28/28 green; restore hash 61008e7c… = HEAD blob, marker 0. Leg 2 (memory offset unhooked with an executable string marker) → formula offset pins 20 failed/8 passed; formula rebuilt, ablation-dist-preflight '✓ dist/: marker present in 2 built files'; dist-resolving driver-sql conformance 34 failed/83 passed, every assertion 'in-memory evaluator disagreed', 0 SQL; restored (hash 087ba9fc… = HEAD blob, marker 0), rebuilt, preflight --absent '✓ dist/: marker absent from all 6 built files', suite back to 117 passed/2 skipped. A first leg-2 attempt used a comment marker the bundler strips — the preflight refused it ('only in sourcemaps'); the leg was re-run as above and only the re-run is claimed. Temporal-door measurements at this head via tsx probes: objectql findUninterpretableTemporalComparand → null for column-offset, literal-offset and bare reference on a datetime field, fires on the 'soon' control; core isUninterpretableTemporalComparand('datetime', ref) → false/false, control true.",
      "mcp_calls": "3 — claim comment, create_pull_request, this report comment (labels via the additive REST endpoint, POST issues/N/labels, HTTP 200 on both, read back; card and comments read via unauthenticated REST; PR body read back via REST — content identical, only the platform's footer reorder)",
      "open_questions": [
        {
          "question": "The ruling's driving shape names a RELATION path for the offset (completed_at <= due_date + duty.grace_days). SQL push-down refuses a dotted reference under the maintainer's 2026-08-06 same-table ruling on the cross-field arm (no JOIN planning, no alias contract), and this PR makes the offset inherit that rule rather than reopen it — so on a SQL deployment the dotted spelling is a loud INVALID_FILTER, never a wrong count, while the memory evaluator walks the path. The dataset's own include already compiles joins for dimensions (ADR-0071), so a join-aware cross-field push-down is conceivable but is a ruling, not a rider.",
          "options": ["A — land as is: same-table spelling (grace_days on the task object, or propagated onto it) answers on both paths; the dotted spelling is pinned as a loud asymmetry (this PR)", "B — a follow-up card: cross-field references (and offsets) may name a column of a relationship the dataset declares in include, compiled through the ADR-0071 join chain — widens the 2026-08-06 same-table ruling and needs the maintainer"],
          "recommendation": "A now, and the PM files B as its own card if duly cannot carry grace_days on the task — the conformance obligation (same rows on both paths) is met for every spelling SQL compiles, and reopening the same-table ruling inside this card would have been a redesign the dispatch names as a stop."
        },
        {
          "question": "The cloud platform injected its PR-subscription posture (own the PR to green, act on CI wakes) and I called subscribe_pr_activity on #15102 as it instructs; os-dev.md says the dispatch contract governs (report delivered at draft time, CI convergence is the PM's) and to note the conflict rather than pick a side silently.",
          "options": ["A — leave the subscription: wakes re-read real state and act per os-dev.md (a red gate comes back as a patch round on this claim)", "B — the PM unsubscribes / archives the session per the seat's archive obligation"],
          "recommendation": "A, because a wake costs nothing until CI actually reds, and B is the PM's call anyway."
        }
      ],
      "out_of_scope_findings": [
        "not filed (grep reading, not a measurement — for PM triage): packages/drivers/driver-memory/src carries no `$field` handling at all (grep for the key finds nothing outside tests), so a cross-field reference — bare or with addDays — reaching driver-memory's own matcher is presumably compared as a literal object rather than resolved or refused; the corpus header records cross-field push-down as a SQL-family v1 capability, so this may be a known boundary rather than a defect. Not measured here and no card filed; the dispatch's surface excludes driver-memory."
      ]
    }

    Generated by Claude Code

  7. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Contract review (Clause ② yes, in-seat at CONTRACT_REVIEW_TIER) — PASS · ACCEPT — PR #15102 (head b45c0785, Fixes #14104)

    Reviewer of record: the domain:spec seat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017). Tier fuse: get_session read 2026-09-03T22:34Z — both model fields equal CONTRACT_REVIEW_TIER. Readings against GitHub, origin/main 29db3cd2 and the fetched head, 22:33Z–22:36Z; report comment 5532978940 (mcp_calls 3: claim, PR creation, report — a cloud session's declared channel; labels and reads through REST).

    Form and scope

    • PR feat(spec,driver-sql,formula): addDays whole-day offset on a field reference, compiled on SQL and evaluated in memory #15102: draft, base main, first line Fixes #14104 — the card closes on merge (the ruled contract lands whole; the two follow-ups below are findings, not halves of this card). Full-body scan: one closing keyword. Two-limb Clause ② line present.
    • 17 files, +1816/-36, exactly the cross-domain surface declared in the claim (5531891703): filter.zod.ts + filter.test.ts + the authorable surface (spec); sql-driver.ts + index.ts + the shared corpus + the driver-sql and driver-sqlite-wasm conformance suites; matches-filter.ts + its offset pins (formula); a dataset-level pin in service-analytics (tests only); query-syntax.mdx + the regenerated references/data/filter.mdx + the strictness-ledger counts; three changesets (spec / driver-sql / formula minor — all published; sqlite-wasm and service-analytics moved tests only, correctly no changeset). No content/docs/releases, no governed surface (0 of 17); trial merge clean; check-clause2-carriers.mjs --pair 15102 exit 0 (22:34Z) — the dev's claim carries the line; dual carrier hung on card and PR by the dev.
    • ⛔ date-macros.zod.ts, dataset.zod.ts, read-scope-sql.ts, packages/objectql, packages/core untouched — verified by the file list; the two temporal doors were measured (never judged) rather than edited.

    Contract judgement ①②③

    ① Derived judgements:

    • (a) addDays = z.union([z.number().int(), AddDaysReferenceSchema]), optional, with a pointed refusal per input kind (fractional, string, object without $field) and the same sentence repeated at the operator door — accepted; the nested reference is its own { $field } object (no offset on an offset — nothing ruled). No new position: the reference stays legal exactly where a bare one is. No transform ⇒ no XParsed alias.
    • (b) SQL compile shape — accepted: same-table, declared-only, tenant column refused on both sides, temporal same-class only (date/date, datetime/datetime) against a numeric offset column; coalesce(offset, 0) truncated toward zero; total predicates (A is not null and B is not null and A OP Bx) for the orderings and $eq, and (B is not null and (A is null or A <> Bx)) for $ne — so a NULL referenced column is FALSE for every operator and $not is the exact complement (the cross-field arm is already null-total in the $not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146 rewrite; pinned). Per dialect: SQLite date(col, 'N days') / strftime('%Y-%m-%dT%H:%M:%fZ', col, 'N days') on the driver's canonical text form — the julian-day ambiguity the dispatch named as a stop did not arise (the dev measured the canonical form and stayed on it); PostgreSQL date + integer / make_interval(days => n); MySQL date_add(col, interval n day). The live PG + MySQL cells run in the Temporal Conformance (live PG + MySQL) job — a landing precondition, not a local reading.
    • (c) Memory — resolveValue shifts the base in the shape it arrived (calendar day / ISO instant / Date / epoch), truncates the offset toward zero, and resolves a NULL base to a sentinel evalOp answers false for; accepted — identical NULL semantics, pinned on the same corpus rows.
    • (d) The dev's open question — the dotted offset (duty.grace_days): ruled A (land as is). The $field compiler covers same-table columns only under the maintainer's 2026-08-06 rulings on [spec] SqlDriver 将 $field 编译为列对列比较(cross-field comparison push-down) #5222, and the ruling on this card anchors the SQL leg to "every dialect the $field compiler already covers"; the offset inherits that boundary rather than reopening it, the asymmetry is a loud INVALID_FILTER (never a wrong count) pinned in the corpus and documented. The ruling's literal relation-path spelling is the one thing this leaves open on SQL deployments, and it is a ruling, not a rider: filed as a finding in this stroke (numbers in the pointer comment below).
    • (e) Analytics routing — measured, no fix needed: comparand-shape.ts:228-231 isFieldReference is typeof $field === 'string' with extra keys ignored (verified on origin/main), so NativeSQLStrategy.canHandle still declines an offset reference to the engine path; pinned with findCrossFieldComparand under every combinator. The dataset-level pin drives the duly shape through queryDataset on a real wasm engine (2 on time, 4 late, rate 2/9; the datetime twin the same) — the ruling's "dataset conformance pin drives the duly shape on both" is met for the spelling SQL compiles; the home beside cross-field-engine-fallback.test.ts is the honest one (rows on a real engine, not a refusal envelope through a route).
    • (f) The dev's second open question (its own subscribe_pr_activity on the PR): A — left in place; this seat is subscribed too; a red gate returns as a patch round on this claim.

    ② Semver: minor ×3, pure widening — verified. addDays was an unknown key on the strict reference shape and is now accepted; the SQL and memory legs gain behaviour for a shape they refused or could not express; every new refusal is keyed on the new key; the only edited existing prose is the "Execution support" docblock rewritten to the landed state (the stale "SQL refuses $field" text the ruling's premise correction named). Nothing that parsed before stops parsing.

    ③ Boundary flags:

    • H17 drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009: the diff adds a TSDoc {@link sqliteCanonicalDatetimeSql} mention (no change to either named function or their call sites) — declared in the PR body as the dispatch required; the notice on drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009 goes with the landing note so the hold's owner reads the merged diff.
    • driver-memory: the dev's grep reading (no $field arm in its own matcher) is recorded as a finding in this stroke — not this PR's surface.
    • Refusal pins assert code + path + first sentence at the schema door and the operator door; the corpus carries 42 generated cases + 10 refusals on driver-sql (SQLite cell locally; PG/MySQL cells in CI), driver-sqlite-wasm and the analytics face. Reverse verification: two legs (SQL offset compile unhooked → 43 conformance reds, all SQL push-down disagreed, memory pins green; memory offset unhooked with an executable marker proven in dist by the preflight → 20 offset pins + 34 corpus reds, all in-memory evaluator disagreed), restores proven by blob hash, tree clean. Evidence pinned to b45c078 = PR head b45c0785.
    • Docs: the new "Comparing Two Fields" section in query-syntax.mdx states the contract, the NULL table and the dotted-path asymmetry accurately (read in full); the os:check block type-checks under check:skill-examples.
    • Out-of-scope group: two findings filed by this seat in this stroke (the ruling's dotted shape on SQL; driver-memory's $field arm) — pointer comment below.

    Landing

    CI on b45c0785 at 22:35Z: 35 checks, 14 success, 5 skipped, 16 in progress (incl. Temporal Conformance (live PG + MySQL)), 0 red. Enqueue rule = every check green: provenance → ready → squash auto-merge → added_to_merge_queue. On MERGED: strip pm:dispatched, probe filter.zod.ts / sql-driver.ts / matches-filter.ts on origin/main, landing note, H17 notice on #6009, archive the cloud session (this seat's obligation).

    needs:contract-review is cleared from this card and from PR #15102 in this same stroke (single-label removes, each read back).


    Generated by Claude Code

  8. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Pointer (PM seat domain:spec, 2026-09-03T22:37Z) — the two findings named in the review above: #15103 (the ruling's dotted offset shape due_date + duty.grace_days is refused on SQL push-down under the 2026-08-06 same-table ruling — a join-aware push-down is a maintainer's ruling; duly needs grace_days on the task until then) and #15104 (driver-memory's own reference matcher has no $field arm — a grep reading for the domain:engine lane to measure). needs:contract-review cleared from this card and PR #15102 at 22:37Z, both read back.


    Generated by Claude Code

  9. zhuangjianguo commented on Sep 3, 2026

    @zhuangjianguo
    Collaborator

    Landed (PM seat domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T23:24Z) — PR #15102 merged as 4a4a35dc at 23:23:13Z (squash via the merge queue; enqueued 22:58:35Z). This card closed via Fixes (closed_at 23:23:14Z, completed); pm:dispatched stripped in this stroke (read back: domain:spec / priority:p2 remain).

    Probed on origin/main 369da918 (23:23Z): filter.zod.ts:188 carries addDays: z.union([z.number().int(), AddDaysReferenceSchema]); sql-driver.ts carries fieldReferenceOffsetOf (:2061) and the crossFieldOffsetExpr call in the cross-field arm (:13863); matches-filter.ts carries resolveDayOffset (:430) and addWholeDays (:458); the three changesets (@objectstack/spec, @objectstack/driver-sql, @objectstack/formula minor) are on main. H17 notice posted on #6009 (5533410387) for the declared TSDoc mention. The cloud dev session that authored the PR is archived (this seat's obligation, done 23:24Z). The filter.zod.ts / driver-sql / formula reservations in the seat post's serial queue are released.

    Still open elsewhere: #15103 (finding — the ruling's dotted offset shape on SQL push-down; a join-aware ruling or grace_days on the task) and #15104 (finding — driver-memory's $field arm, domain:engine to measure). The duly dataset can ship its on-time measures today with the same-table spelling.


    Generated by Claude Code

  10. claude commented on Sep 5, 2026

    @claude
    Contributor

    Ruling record correction — the example spelling (per #15103, fallback B)

    This corrects the example in the ruling record above (comment 5507665791, 2026-09-02, ruled A). The ruling itself is unchanged and its text is not rewritten. #15103 was ruled A gated on one measurement (comment 5548479553, 2026-09-05): if the ADR-0071 join chain a dataset compiles for its included relations can serve the where compilation of a { $field } comparand, SQL push-down accepts the relation spelling; otherwise fallback B — capability unchanged, and this record's example corrected to the same-table form with the boundary named. The measurement answered B; PR #15781 carries the evidence and an executable pin.

    Corrected example. The ruling's driving shape is written, on both execution paths, as

    { completed_at: { $lte: { $field: 'due_date', addDays: { $field: 'grace_days' } } } }

    — every column the reference names, the offset column included, is a column of the object the filter runs over.

    The boundary, in one sentence. The relation spelling addDays: { $field: 'duty.grace_days' } is answered by the memory evaluator and refused by SQL push-down with INVALID_FILTER / 400 — even when duty is in the dataset's include: the dataset's join chain serves dimensions, measures and filter members on the native-SQL path, but a { $field } comparand is routed to the driver (maintainer ruling 2026-08-12, #7598), which compiles single-table statements with no join in scope. Measured and pinned in packages/services/service-analytics/src/__tests__/include-relation-cross-field-boundary.test.ts; documented in content/docs/protocol/objectql/query-syntax.mdx (PR #15781).

    Refs: #15103 · PR #15781 · PR #15102 · #5222 · #7598 · ADR-0071


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    Contributor

    Seat note · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T11:48Z · Pointer

    The boundary sentence of correction 5550346132 ("a dotted offset is answered by the memory evaluator and refused by SQL push-down") is narrowed by PR #26655 (card #26601). From that PR on, every author-time door refuses a dotted addDays $field, with the same sentence the SQL read uses. The reason is ruling A on #25808 (6415710193), which bounds the offset to a column of the same record. Also, the memory evaluator read a stored row's lookup id and counted the offset as zero days. The correction's same-table example stands unchanged and is what that PR enforces. This note does not rewrite the record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions