Repository navigation
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
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-computedgrace_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:
- 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. - A derived-measure operator over a date difference — the layer already has
derived: { op, of }for combining measures by name; adays_betweenmeasure plus an
ordering test would be a natural extension of that vocabulary. - 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
huangyiirene commentedon Sep 2, 2026
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):
git grep -n "FILTER_OPERATORS" origin/main -- packages/spec/src/data/filter.zod.ts(阳性对照:同文件$lte非 0)。$field列比列在 SQL 驱动上以INVALID_FILTER/ 400 拒绝、memory 驱动可算 ——git grep -n "#5041" origin/main -- packages/spec/src/data/filter.zod.ts。$field编译为列对列比较(cross-field comparison push-down) #5222(SQL 端编译$field)的现状 —— 现读 open/closed,⛔ 不引本卡写作时的描述。选项 × 真实代价:
{ $lte: { $field: 'due_date', addDays: { $field: 'duty.grace_days' } } },骑在 #5222 上derived: {op, of})业务含义直译: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
os-project-manager commentedon Sep 2, 2026
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
$fieldalready compiles to a column-to-column comparison ondriver-sql. The only remaining gap is date arithmetic. If the "Execution support (#5041)" block infilter.zod.tsstill says the SQL path refuses$field, that prose is out of date and moves with this card.Ruled: A.
FieldReferenceSchemagains 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 NULLdue_datemakes the comparison false (no deadline, never "on time"), both spelled out rather than inherited from SQL three-valued logic. Compiles on every dialect the$fieldcompiler already covers and evaluates identically on the memory evaluator; the dataset conformance pin drives thedulyshape (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:speclane;Clause-②: yes(the filter grammar widens), dispatch atCONTRACT_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
zhuangjianguo commentedon Sep 3, 2026
Claim: PM loop,
domain:specseat (seat post #6017), R3 of this shift — the cross-domain single PR the ruling designates to this lane (spec grammar + thedriver-sqlcompiler + the memory evaluator)Session:
session_0174WZTU6XcFcS7g2kykC53i(GitHubzhuangjianguo)Branch:
claude/issue-14104-field-reference-add-daysWorktree:
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):
packages/spec/src/data/filter.zod.ts—FieldReferenceSchema(:100-102) gainsaddDays: an integer literal of any sign, or a nested{ $field }reference; the describe states the ruled NULL semantics; the "Execution support ([spec]$field跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041)" docblock (:39-64, which still says SQL push-down refuses$fieldand points at [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222) is rewritten to the landed state (PR feat(driver-sql): compile $field to column-to-column comparison (#5222) #7582 compiles it; the offset rides on the same arm). No new positions: the reference stays legal exactly where it is today (scalar comparands of the equality and ordering operators); list positions keep the [spec]FieldReferenceSchemais declared in the$betweenendpoints but NO backend resolves a$fieldinside a list #7596 refusal.packages/spec/src/data/filter*.test.ts— accept pins (literal, negative literal, nested reference, dot-path reference); refusal pins withcode+path+ first sentence (non-integer, string,addDaysbeside a missing$field); a positive control; an ADR-0122XParsedalias only if a transform is introduced (none expected).packages/drivers/driver-sql/src/sql-driver.ts— the cross-field arm PR feat(driver-sql): compile $field to column-to-column comparison (#5222) #7582 landed (fieldReferenceOf:2032; compile sites around:2021-2134) compilescolumn OP other_column + offsetper dialect,COALESCE(offset, 0)for a NULL offset column, andother_column IS NOT NULL AND …so a NULL base is FALSE (not NULL) and stays false under$not; the SQLite arm must add days on the driver's canonical datetime representation (read thesqliteCanonicalDatetimeSqlregion — see the H17 note below).packages/drivers/driver-sql/src/cross-field-conformance-cases.ts+sql-driver-cross-field-conformance.test.ts+sql-driver-cross-field-reference.test.ts— the shared corpus gains the offset rows (literal, column, negative, NULL offset, NULL base,$not-wrapped), same expected id sets on both paths;packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-cross-field-conformance.test.tsruns them through the inherited compiler (no compiler change there); the live PG + MySQL leg goes wherever theTemporal Conformance (live PG + MySQL)job runs dialect suites.packages/formula/src/matches-filter.ts—resolveValue(:381-384) resolves the offset (literal or reference) and adds whole days to the resolved base; identical NULL semantics;matches-filter*.test.tspins from the same corpus.packages/services/service-analytics/src/comparand-shape.tsisFieldReference(:313, the detector that makesNativeSQLStrategy.canHandledecline a cross-field comparison so a dataset filter routes to the engine path — maintainer ruling 2026-08-12 Q1 = B,analytics-service.ts:2173-2193): if it is key-count-strict, a reference carryingaddDayswould stop being detected and be bound as JSON, so it must keep detecting;packages/objectql/src/temporal-comparand-door.ts:136-138andpackages/core/src/utils/temporal-comparand.ts:138(the door already treats a$fieldobject as "never judged" — confirm the extra key changes nothing).completed_at <= due_date + duty.grace_days) as a dataset measure filter, driven through both paths — the conformance corpus covers the filter; the dataset-level pin lives beside the existing analytics dataset tests (thepackages/rest/src/analytics-dataset-*.test.tsfamily or the analytics service's own suite — the dev picks the honest home and says why).content/docs/protocol/objectql/query-syntax.mdx(the$fieldparagraph gains the offset); generatedcontent/docs/references/data/filter.mdx, JSON schema and authorable surface viacheck:generated --fix;check-system-context-census.mjs --fixif an anchor rots (filter.zod.tsandsql-driver.tsboth carry anchored declarations).@objectstack/specminor,@objectstack/driver-sqlminor,@objectstack/formulaminor (all published at 17.2.0,privateunset);driver-sqlite-wasmonly if its own code moves.date-macros.zod.ts(no new macro — the offset is anchored to a column, never to now);dataset.zod.ts(no option B or C);packages/objectql/packages/corebeyond the reads above;read-scope-sql.ts(read scopes keep their refusal — not caller-authored);content/docs/releases;skills.Container & model: L,
mode:cloud(L is the cloud-reserved tier),model: fable(CONTRACT_REVIEW_TIER).node scripts/pm/dispatch-gates.mjs --tieron the four primary files ate849a0af(based17f352b;origin/mainbe416187; 20:42Z): "no path-derived mandate" + "Clause ② SUSPECT surface" (filter.zod.ts⇢packages/spec/src/**); tier decided from content — the filter grammar widens (a new key on a published accept set).Clause-②: yes — public-surface widening; ⛔ no pre-hang, the dual carrier is hung with the PR; the dev's own claim comment must carry the same
Clause-②: yesline (the enqueue gate's content limb reads the dev's claim, not this one — The most common action in any app — set a field on the current record — has no declarative form for a ROW action, while the BULK form is fully declarative #14092's lesson today).Serial constraints cleared: open-PR file scan at 20:38Z (25 open PRs, 209 files): none touches
driver-sql,matches-filter,filter.zod.ts,packages/formulaor a field-reference file (positive control: 8packages/spec/paths in the same list); lane siblings in flight (spec: interface pages acceptappearance.allowedVisualizations: ['calendar']but the page surface has NO calendar binding key at all — the #13748 shape on a different door #14073packages/lint, Sharing rules cannot name a RECORD-RELATIVE recipient (the owner's manager; the value of a user field on the matched record), and no other declarative surface can widen a private object either #14103security/sharing.*, PR feat(spec): row-level declarative field write — operation: 'update' + patch on ActionSchema #15077ui/action.zod.ts+stack.zod.ts, PR docs(spec): state the measured dependsOn spelling facts in the option-description docblock #15055data/field.zod.ts) file-disjoint; targeted cross-domain in-flight check (domain:engineclaims ondriver-sql/formula): none among the open PRs' files;filter.zod.tslast moved by PR docs(spec): describe() for SpecialOperator $null and $exists #15054 (fc58a99e, 20:05Z) — this branch starts fromorigin/mainafter it; no remote branch exists for this card (20:42Z).H17 on-hold trigger-file index ([Half-state patrol] check-half-states live sweep — generated view (please pin) #9857 body, read 20:38Z):
packages/drivers/driver-sql/src/sql-driver.tsis declared by drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009 (Restart-when: any PR touchessqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimes), drivers(sql):upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740 (insertOnlyUpsertColumnsgains a member — not this surface) and [finding] On SQLite, applyMigrationEntries runs a FULL table rebuild even when zero entries in the batch are honourable #12132 (Restart-touch; the schema differ — not this surface). Notices go on the three hold cards in this stroke; the dispatch tells the dev that touching either drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009 symbol must be declared in the PR body (the hold's owner re-evaluates; no rider work here).Decision re-read (2 comments): triage 5503093481 (decision inbox, four-facet block, recommendation A with fallback D); maintainer ruling 5507665791 (2026-09-02, director summon Convert to monorepo with scoped packages #8, verbatim 「13973 帮我综合分析,并参考主流平台的方案,并给我解释为什么不能都用日期类型。其他同意」 — 「其他同意」 adopts A):
FieldReferenceSchemagainsaddDays(a column reference, a literal number also accepted); a NULL offset column contributes zero days; a NULL base makes the comparison false — both spelled out in the describe and pinned in the$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 shape; compiles on every dialect the$fieldcompiler covers and evaluates identically in memory; the dataset conformance pin drives thedulyshape on both; B and C not taken, D superseded;Clause-②: yes,CONTRACT_REVIEW_TIER; PR feat(driver-sql): compile $field to column-to-column comparison (#5222) #7582 named as the base; the memory-evaluator leg required beside the SQL one. Premise correction carried: [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 is CLOSED (PR feat(driver-sql): compile $field to column-to-column comparison (#5222) #7582, 2026-08-11 —sql-driver.ts+351, the shared corpus, the two driver suites), so part 2 of the card is stale and thefilter.zod.ts:39-64prose moves with this card.Premise re-read on
origin/mainbe416187(20:39Z):FieldReferenceSchemais{ $field: string }only;orderingComparandSchema(:262) admits it;sql-driver.ts:2032fieldReferenceOfand the cross-field compile exist;matches-filter.ts:383resolves$fieldwith no offset; noaddDaysinfilter.zod.ts(grep, 20:39Z). Premise holds; nothing of the ruling has landed.Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Dispatch (R3 of this shift, 2026-09-03T20:46Z) —
domain:specseat,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 sessionsession_019vx3536MUFc8XYVLNoKhs3(created 20:45Z, outcome branchclaude/issue-14104-field-reference-add-days; the archive obligation is this seat's). The dev leaves its ownClaim:comment below carrying theClause-②: yesline; 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):
FieldReferenceSchemagainsaddDays(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 withcode+path+ first sentence; generated artifacts via--fix;query-syntax.mdxgains the offset.column OP (other + COALESCE(offset, 0) days)underother 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), thedriver-sqlanddriver-sqlite-wasmsuites run them, the live PG + MySQL leg goes where that CI job runs.matches-filter.tsresolveValueadds whole days with identical NULL semantics; pins from the same rows.service-analyticscomparand-shape.tsisFieldReference(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 carriesaddDays; the temporal comparand doors unchanged. Dataset-level pin of thedulyshape beside the existing analytics dataset tests.--fix; H17: touchingsqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimesmust be declared in the PR body (drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009), no rider work.Part of #14104+ the fork stated); any file outside the declared surface.Fixes #14104, two-limb Clause ② line,needs:contract-reviewon PR and card in the same stroke with readbacks, ⛔ no ready flip; report asos-dev-reporthere. Self-drive clause for the cloud session: no turn ends on a question or an interim report.Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Claim:
os-devdeveloper agent, dispatched by thedomain:specPM seat (seat post #6017, dispatch 5531920565)Session:
session_019vx3536MUFc8XYVLNoKhs3(cloud,mode:cloud, size L)Branch:
claude/issue-14104-field-reference-add-days(pushed empty at1c7adc7=origin/mainas the write-route probe; probe answered 200)Worktree:
../objectstack-issue-14104, base1c7adc73(origin/mainat claim time, after PR #15054'sfc58a99elanding onfilter.zod.ts)Ruling followed: maintainer ruling A as recorded in 5507665791 —
FieldReferenceSchemagainsaddDays(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 inmatches-filter.ts, conformance corpus rows on both paths, dataset pin of thedulyshape.File surface: exactly the one declared in the PM claim 5531891703 (spec grammar + pins + generated artifacts +
query-syntax.mdx;driver-sqlcompiler + shared corpus + the two driver conformance suites +driver-sqlite-wasm's inherited run;formulaevaluator + pins; a read-and-measure ofservice-analytics'isFieldReferencedetector 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
zhuangjianguo commentedon Sep 3, 2026
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
zhuangjianguo commentedon Sep 3, 2026
Contract review (Clause ② yes, in-seat at
CONTRACT_REVIEW_TIER) — PASS · ACCEPT — PR #15102 (headb45c0785,Fixes #14104)Reviewer of record: the
domain:specseat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017). Tier fuse:get_sessionread 2026-09-03T22:34Z — both model fields equalCONTRACT_REVIEW_TIER. Readings against GitHub,origin/main29db3cd2and 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
main, first lineFixes #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.+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 + thedriver-sqlanddriver-sqlite-wasmconformance suites;matches-filter.ts+ its offset pins (formula); a dataset-level pin inservice-analytics(tests only);query-syntax.mdx+ the regeneratedreferences/data/filter.mdx+ the strictness-ledger counts; three changesets (spec/driver-sql/formulaminor — all published;sqlite-wasmandservice-analyticsmoved tests only, correctly no changeset). Nocontent/docs/releases, no governed surface (0 of 17); trial merge clean;check-clause2-carriers.mjs --pair 15102exit 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/coreuntouched — verified by the file list; the two temporal doors were measured (never judged) rather than edited.Contract judgement ①②③
① Derived judgements:
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 ⇒ noXParsedalias.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$notis 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: SQLitedate(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); PostgreSQLdate + integer/make_interval(days => n); MySQLdate_add(col, interval n day). The live PG + MySQL cells run in theTemporal Conformance (live PG + MySQL)job — a landing precondition, not a local reading.resolveValueshifts 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 sentinelevalOpanswers false for; accepted — identical NULL semantics, pinned on the same corpus rows.duty.grace_days): ruled A (land as is). The$fieldcompiler 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$fieldcompiler already covers"; the offset inherits that boundary rather than reopening it, the asymmetry is a loudINVALID_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 afindingin this stroke (numbers in the pointer comment below).comparand-shape.ts:228-231isFieldReferenceistypeof $field === 'string'with extra keys ignored (verified onorigin/main), soNativeSQLStrategy.canHandlestill declines an offset reference to the engine path; pinned withfindCrossFieldComparandunder every combinator. The dataset-level pin drives thedulyshape throughqueryDataseton a real wasm engine (2 on time, 4 late, rate 2/9; thedatetimetwin the same) — the ruling's "dataset conformance pin drives the duly shape on both" is met for the spelling SQL compiles; the home besidecross-field-engine-fallback.test.tsis the honest one (rows on a real engine, not a refusal envelope through a route).subscribe_pr_activityon 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.
addDayswas 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:
{@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$fieldarm in its own matcher) is recorded as afindingin this stroke — not this PR's surface.code+path+ first sentence at the schema door and the operator door; the corpus carries 42 generated cases + 10 refusals ondriver-sql(SQLite cell locally; PG/MySQL cells in CI),driver-sqlite-wasmand the analytics face. Reverse verification: two legs (SQL offset compile unhooked → 43 conformance reds, allSQL 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, allin-memory evaluator disagreed), restores proven by blob hash, tree clean. Evidence pinned tob45c078= PR headb45c0785.query-syntax.mdxstates the contract, the NULL table and the dotted-path asymmetry accurately (read in full); theos:checkblock type-checks undercheck:skill-examples.$fieldarm) — pointer comment below.Landing
CI on
b45c0785at 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: strippm:dispatched, probefilter.zod.ts/sql-driver.ts/matches-filter.tsonorigin/main, landing note, H17 notice on #6009, archive the cloud session (this seat's obligation).needs:contract-reviewis cleared from this card and from PR #15102 in this same stroke (single-label removes, each read back).Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Pointer (PM seat
domain:spec, 2026-09-03T22:37Z) — the two findings named in the review above: #15103 (the ruling's dotted offset shapedue_date + duty.grace_daysis refused on SQL push-down under the 2026-08-06 same-table ruling — a join-aware push-down is a maintainer's ruling;dulyneedsgrace_dayson the task until then) and #15104 (driver-memory's own reference matcher has no$fieldarm — a grep reading for thedomain:enginelane to measure).needs:contract-reviewcleared from this card and PR #15102 at 22:37Z, both read back.Generated by Claude Code
zhuangjianguo commentedon Sep 3, 2026
Landed (PM seat
domain:spec, session_0174WZTU6XcFcS7g2kykC53i, 2026-09-03T23:24Z) — PR #15102 merged as4a4a35dcat 23:23:13Z (squash via the merge queue; enqueued 22:58:35Z). This card closed viaFixes(closed_at23:23:14Z,completed);pm:dispatchedstripped in this stroke (read back:domain:spec/priority:p2remain).Probed on
origin/main369da918(23:23Z):filter.zod.ts:188carriesaddDays: z.union([z.number().int(), AddDaysReferenceSchema]);sql-driver.tscarriesfieldReferenceOffsetOf(:2061) and thecrossFieldOffsetExprcall in the cross-field arm (:13863);matches-filter.tscarriesresolveDayOffset(:430) andaddWholeDays(:458); the three changesets (@objectstack/spec,@objectstack/driver-sql,@objectstack/formulaminor) are onmain. 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). Thefilter.zod.ts/driver-sql/formulareservations 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 orgrace_dayson the task) and #15104 (finding— driver-memory's$fieldarm,domain:engineto measure). Thedulydataset can ship its on-time measures today with the same-table spelling.Generated by Claude Code
claude commentedon Sep 5, 2026
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 thewherecompilation 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 withINVALID_FILTER/ 400 — even whendutyis in the dataset'sinclude: 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 inpackages/services/service-analytics/src/__tests__/include-relation-cross-field-boundary.test.ts; documented incontent/docs/protocol/objectql/query-syntax.mdx(PR #15781).Refs: #15103 · PR #15781 · PR #15102 · #5222 · #7598 · ADR-0071
Generated by Claude Code
docs(spec): three prose carriers now state what the tree does, not a …
docs(spec): three prose carriers state what the tree does, not a prem…
test(spec): the fourth data/ file group's test titles state each cite…
objectstack-fleet commentedon Oct 27, 2026
Seat note ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T11:48Z · PointerThe 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 dottedaddDays$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.