You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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?"
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 readduty.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:
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.
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.
Filed from the duly dogfood application (objectstack-ai/duly#9), which is shipping the
dataset with the affected measures omitted rather than approximated.
Summary
A dataset measure (
ui/dataset.zod.ts, ADR-0021) cannot express the single most commongovernance 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
dulyapplication (objectstack-ai/duly#9):completed_atanddue_dateare columns on the base objectduly_task.grace_daysis anumbercolumn on the relatedduly_duty. All three are stored andindexed. 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 aduty.<field>path is exactlywhat the semantic layer is for: joins are compiled from
include(ADR-0071, ≤3 hops) andthe author writes no ON clause.
duly_duty_healthships afrequencydimension bound toduty.frequencytoday 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_dateplus 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_datehas no usablespelling either.
FieldReferenceSchemadeclares it:but
filter.zod.ts's own "Execution support (#5041)" block records thatdriver-sql(anddriver-sqlite-wasm, which inherits its compiler) reject it withINVALID_FILTER/ HTTP400, 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
$fieldwould answer on a memory-driver deployment and 400 on a SQL one — the semanticlayer's correctness would become a property of the deployment.
So even if #5222 landed tomorrow, part 1 would still leave
due_date + duty.grace_daysunspellable. 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:
grace_days(or a pre-computedgrace_deadline) onto the task. A secondwriter 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.
semantic layer, where no dashboard widget can bind it — which defeats the point of
ADR-0021 — and duplicates the definition per consumer.
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_daysis authored onduly_catalog_item, propagated toduly_dutyatinstantiation, 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:
{ $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.derived: { op, of }for combining measures by name; adays_betweenmeasure plus anordering test would be a natural extension of that vocabulary.
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/dulyatclaude/issue-9-analytics-datasets,pnpm validate. Add tosrc/datasets/duty-health.dataset.ts:This parses and validates clean — the schema accepts
$field— and then refuses atruntime on any SQL driver. There is no variant of it that adds the grace offset at all.
Environment
@objectstack/spec17.2.0,@objectstack/cli17.2.0.Filed from the
dulydogfood application (objectstack-ai/duly#9), which is shipping thedataset with the affected measures omitted rather than approximated.